diff --git a/go/delivery/assigner.go b/go/delivery/assigner.go index 46560f2..b009088 100644 --- a/go/delivery/assigner.go +++ b/go/delivery/assigner.go @@ -1,6 +1,7 @@ package delivery import ( + "bytes" "encoding/json" "fmt" "reflect" @@ -16,6 +17,10 @@ import ( // only compared; the node agent applying them is release 6 (ADR 0429 §7). Only the assigner's credential writes these // buckets; the watchman, the view and the controller read them. // +// **Release 6** (ADR 0539 clauses 1, 6 and 16): a want also names the digest of the one composition it allows, written +// only once that composition exists (the seventh rule, RuleComposed), and carries the answer to a failed backup; the +// node's own want `want..node` names the digest of the node's own composition (NodeWant). +// // **The bucket is the first word, the key the rest** (as the builder's `builds..` is the key // `.` in its builds bucket): the differences bucket's keys are `.`. The wants bucket holds // two kinds of key, so each keeps its word: `want..` and `held..`. @@ -50,7 +55,33 @@ func WantKey(node, module string) (string, error) { return wantWord + "." + have, nil } -// ParseWantKey is the inverse of WantKey; it refuses a held key, a have key and any key of another bucket, naming it. +// WantNodeKey is the key, in AssignerWantsBucket, of a node's own want: `want..node`, naming the digest of the +// node's own composition (ADR 0539 clause 6; ADR 0429 §4's node key). The assigner writes it once the node's own +// composition exists. +func WantNodeKey(node string) (string, error) { + own, err := HaveNodeKey(node) + if err != nil { + return "", err + } + return wantWord + "." + own, nil +} + +// ParseWantNodeKey is the inverse of WantNodeKey; it refuses a module's want key and any key of another bucket, +// naming it. +func ParseWantNodeKey(key string) (node string, err error) { + rest, ok := strings.CutPrefix(key, wantWord+".") + if !ok { + return "", fmt.Errorf("delivery: %q is not a node's own want key, want..node", key) + } + node, _, isNode, err := ParseHaveKey(rest) + if err != nil || !isNode { + return "", fmt.Errorf("delivery: %q is not a node's own want key, want..node", key) + } + return node, nil +} + +// ParseWantKey is the inverse of WantKey; it refuses a held key, a have key, the node's own want key and any key of +// another bucket, naming it. func ParseWantKey(key string) (node, module string, err error) { rest, ok := strings.CutPrefix(key, wantWord+".") if !ok { @@ -100,7 +131,8 @@ func ParseDifferenceKey(key string) (node, module string, err error) { return node, module, nil } -// The six rules of ADR 0429 §6, by number, as a want or a difference names the one that holds a node back. +// The rules of ADR 0429 §6 and ADR 0539 clause 6, by number, as a want or a difference names the one that holds a node +// back. const ( RuleBuildDone = 1 // the build of F is done RuleNotHeld = 2 // F is not held @@ -108,15 +140,25 @@ const ( RuleStep = 4 // at most step other nodes are ahead (step 0: no limit, ADR 0528 §3) RuleProviders = 5 // every provider the module requires runs healthy on that node, or at its canary RuleGrant = 6 // the module's credential for F exists (grants..) + RuleComposed = 7 // the module's composition for that node at F exists (mesh-controller_composed, ADR 0539 §6) ) func checkRule(rule int, of string) error { - if rule < RuleBuildDone || rule > RuleGrant { - return fmt.Errorf("delivery: %s names the rule %d, which is not one of the six (1 to 6)", of, rule) + if rule < RuleBuildDone || rule > RuleComposed { + return fmt.Errorf("delivery: %s names the rule %d, which is not one of the seven (1 to 7)", of, rule) } return nil } +// The answers a want carries to a failed backup before an apply (ADR 0539 clause 16): the node agent reads them from +// its own want and from nothing else. +const ( + // BackupAgain is the answer to back the module up again before the apply. + BackupAgain = "again" + // BackupSkip is the answer to go on without a backup. + BackupSkip = "skip" +) + func checkFingerprint(fp, of string) error { if _, err := FingerprintKeyPart(fp); err != nil { return fmt.Errorf("delivery: %s: %w", of, err) @@ -124,29 +166,44 @@ func checkFingerprint(fp, of string) error { return nil } -// Want is a node's want for a module: the fingerprint FP the node should run, or Removed for a module that left the -// node's modules (then FP is empty). When the module's newest fingerprint is not yet allowed on this node, Newest names -// it and Rule the rule that holds the node back; both are empty when FP is the newest. Words say why in a sentence. -// The entry's own time says since when. +// Want is a node's want for a module: the fingerprint FP the node should run and the digest Composed of the one +// composition of it the node may apply (ADR 0539 clause 1), or Removed for a module that left the node's modules (then +// both are empty). When the module's newest fingerprint is not yet allowed on this node, Newest names it and Rule the +// rule that holds the node back; both are empty when FP is the newest. Backup is the answer to a failed backup before +// the apply, BackupAgain or BackupSkip (clause 16), empty when none was asked. Words say why in a sentence. The +// entry's own time says since when. type Want struct { - V int `json:"v"` - FP string `json:"fp,omitempty"` - Removed bool `json:"removed,omitempty"` - Newest string `json:"newest,omitempty"` - Rule int `json:"rule,omitempty"` - Words string `json:"words,omitempty"` + V int `json:"v"` + FP string `json:"fp,omitempty"` + Composed string `json:"composed,omitempty"` + Removed bool `json:"removed,omitempty"` + Newest string `json:"newest,omitempty"` + Rule int `json:"rule,omitempty"` + Backup string `json:"backup,omitempty"` + Words string `json:"words,omitempty"` } func (w Want) check() error { if w.Removed { - if w.FP != "" || w.Newest != "" || w.Rule != 0 { - return fmt.Errorf("delivery: a removed want names no fingerprint and no rule") + if w.FP != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" { + return fmt.Errorf("delivery: a removed want names no fingerprint, composition, rule or backup answer") } return nil } if err := checkFingerprint(w.FP, "a want's fingerprint"); err != nil { return err } + if w.Composed == "" { + return fmt.Errorf("delivery: the want of %s names no composition: a want names the digest of the one composition it allows (ADR 0539 clause 1)", w.FP) + } + if err := checkDigest(w.Composed, "a want's composition digest"); err != nil { + return err + } + switch w.Backup { + case "", BackupAgain, BackupSkip: + default: + return fmt.Errorf("delivery: the backup answer %q is not %s or %s", w.Backup, BackupAgain, BackupSkip) + } if w.Newest == "" && w.Rule == 0 { return nil } @@ -236,6 +293,41 @@ func DecodeWant(data []byte) (Want, error) { // SameWant says whether two wants are the same value, so the assigner writes only when they differ. func SameWant(a, b Want) bool { return a == b } +// NodeWant is a node's own want (WantNodeKey): the digest Composed of the node's own composition it may apply. Words +// say why in a sentence. +type NodeWant struct { + V int `json:"v"` + Composed string `json:"composed"` + Words string `json:"words,omitempty"` +} + +// EncodeNodeWant is w as JSON at Version; it refuses a digest not of the sha256 form. +func EncodeNodeWant(w NodeWant) ([]byte, error) { + if err := checkDigest(w.Composed, "the node's own want's composition digest"); err != nil { + return nil, err + } + w.V = Version + return json.Marshal(w) +} + +// DecodeNodeWant reads a node's own want; it refuses an unknown version (ErrUnknownVersion), a field a node's own want +// does not have (a fingerprint among them), and what EncodeNodeWant refuses. +func DecodeNodeWant(data []byte) (NodeWant, error) { + if err := checkVersion(data); err != nil { + return NodeWant{}, err + } + var w NodeWant + d := json.NewDecoder(bytes.NewReader(data)) + d.DisallowUnknownFields() + if err := d.Decode(&w); err != nil { + return NodeWant{}, fmt.Errorf("delivery: a node's own want does not fit its shape: %w", err) + } + if err := checkDigest(w.Composed, "the node's own want's composition digest"); err != nil { + return NodeWant{}, err + } + return w, nil +} + // EncodeHeld is h as JSON at Version; it refuses a node name a key refuses. func EncodeHeld(h Held) ([]byte, error) { if err := h.check(); err != nil { diff --git a/go/delivery/assigner_test.go b/go/delivery/assigner_test.go index 3a2d5cc..a63f108 100644 --- a/go/delivery/assigner_test.go +++ b/go/delivery/assigner_test.go @@ -74,8 +74,8 @@ func TestDifferenceKey(t *testing.T) { // 1297/B1a: wants, holds and differences round-trip through their encoders unchanged, at Version. func TestAssignerValuesRoundTrip(t *testing.T) { wants := []Want{ - {FP: fp2of('a')}, - {FP: fp2of('a'), Newest: fp2of('b'), Rule: RuleCanary, Words: "the canary ace does not run it healthy yet"}, + {FP: fp2of('a'), Composed: hashOf('1')}, + {FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: RuleCanary, Words: "the canary ace does not run it healthy yet"}, {Removed: true, Words: "the module left g14"}, } for _, w := range wants { @@ -127,7 +127,7 @@ func TestAssignerValuesRefused(t *testing.T) { {Removed: true, FP: fp2of('a')}, {FP: fp2of('a'), Newest: fp2of('b')}, {FP: fp2of('a'), Newest: fp2of('a'), Rule: 3}, - {FP: fp2of('a'), Newest: fp2of('b'), Rule: 7}, + {FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 8}, } { if _, err := EncodeWant(w); err == nil { t.Errorf("EncodeWant(%+v) accepted", w) diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 71ccf75..128c683 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -90,6 +90,10 @@ const ( // for which it had written a have key; the key is never deleted (issue 1369, ADR 0408 §4), so a reader can tell // that the module stopped. PhaseRemoved Phase = "removed" + // PhaseHeld is a module whose backup before an apply failed: it keeps running what it has, nothing of the new + // composition is applied, and the node waits for the answer in its want (novox/hq ADR 0539 clause 16). Detail + // carries the backup's own words. + PhaseHeld Phase = "held" ) // Health is what the module's own check says. @@ -316,24 +320,31 @@ func PulseTTL(moveAge time.Duration) (ttl time.Duration, stalled bool) { // Have is a module's state on a node, written by that node's node-engine under HaveKey. FP is the module's // fingerprint once the indexer exists (design 47 release 3); until then Build is the build the node was sent, and // the view compares by build. +// +// Composed is the digest of the composition the node runs for the module (novox/hq ADR 0539 clause 9): the +// controller judges a move against it (clause 14), and the assigner reads it to know a want was applied. type Have struct { - V int `json:"v"` - FP string `json:"fp,omitempty"` - Build string `json:"build,omitempty"` - Phase Phase `json:"phase"` - Health Health `json:"health"` - Detail string `json:"detail,omitempty"` + V int `json:"v"` + FP string `json:"fp,omitempty"` + Composed string `json:"composed,omitempty"` + Build string `json:"build,omitempty"` + Phase Phase `json:"phase"` + Health Health `json:"health"` + Detail string `json:"detail,omitempty"` } // HaveNode is a node's own facts, written under HaveNodeKey. Each field is the node-engine's own JSON, carried whole: // this package names the facts and does not type them. +// +// Composed is the digest of the node's own composition it runs (ADR 0539 clause 9). type HaveNode struct { - V int `json:"v"` - Windows json.RawMessage `json:"windows,omitempty"` - Busy json.RawMessage `json:"busy,omitempty"` - Strays json.RawMessage `json:"strays,omitempty"` - Filters json.RawMessage `json:"filters,omitempty"` - Profile json.RawMessage `json:"profile,omitempty"` + V int `json:"v"` + Composed string `json:"composed,omitempty"` + Windows json.RawMessage `json:"windows,omitempty"` + Busy json.RawMessage `json:"busy,omitempty"` + Strays json.RawMessage `json:"strays,omitempty"` + Filters json.RawMessage `json:"filters,omitempty"` + Profile json.RawMessage `json:"profile,omitempty"` } // Presence is whether a node or a part's instance is online; the entry's own time says since when. The watchman @@ -381,9 +392,14 @@ func decode(data []byte, into any) error { func (h Have) check() error { switch h.Phase { - case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved: + case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved, PhaseHeld: default: - return fmt.Errorf("delivery: phase %q is not backing-up, applying, checking, running, failed or removed", h.Phase) + return fmt.Errorf("delivery: phase %q is not backing-up, applying, checking, running, failed, removed or held", h.Phase) + } + if h.Composed != "" { + if err := checkDigest(h.Composed, "the have's composition digest"); err != nil { + return err + } } switch h.Health { case HealthOK, HealthFailed, HealthWaits, HealthUnknown: @@ -458,7 +474,7 @@ func SameHave(a, b Have) bool { return a == b } // SameHaveNode says whether two node facts are the same value, comparing each carried JSON without its spacing. func SameHaveNode(a, b HaveNode) bool { - return a.V == b.V && sameJSON(a.Windows, b.Windows) && sameJSON(a.Busy, b.Busy) && + return a.V == b.V && a.Composed == b.Composed && sameJSON(a.Windows, b.Windows) && sameJSON(a.Busy, b.Busy) && sameJSON(a.Strays, b.Strays) && sameJSON(a.Filters, b.Filters) && sameJSON(a.Profile, b.Profile) } diff --git a/go/delivery/view_answers.go b/go/delivery/view_answers.go index d8f15a3..68561fc 100644 --- a/go/delivery/view_answers.go +++ b/go/delivery/view_answers.go @@ -32,19 +32,27 @@ const ( AskKindHold = "hold" // AskKindRestore is the kind of the assigner's ask about a data module set back. AskKindRestore = "restore" + // AskKindBackup is the kind of the assigner's ask about a backup before an apply that failed: back up again, or go + // on without one (novox/hq ADR 0539 clause 16). + AskKindBackup = "backup" + // AskKindRecord is the kind of the assigner's ask about a move a person must allow, for a module of policy record: + // a composition judged to recreate a container or move data, or one that cannot be judged (ADR 0539 clause 14). + AskKindRecord = "record" viewWord = "view" ) // AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's -// Module and fingerprint FP, a restore's Node, Module and FP. +// Module and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest +// Composed of the composition it would allow. type AskKey struct { - Kind string - Group string - Hash string - Node string - Module string - FP string + Kind string + Group string + Hash string + Node string + Module string + FP string + Composed string } // AskNeedsProof says whether an ask of this kind must prove who answered it, so the view may not answer it: the @@ -53,10 +61,11 @@ func AskNeedsProof(kind string) (bool, error) { switch kind { case AskKindMerge: return true, nil - case AskKindHold, AskKindRestore: + case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord: return false, nil } - return false, fmt.Errorf("delivery: %q is not a kind of ask, %s, %s or %s", kind, AskKindMerge, AskKindHold, AskKindRestore) + return false, fmt.Errorf("delivery: %q is not a kind of ask, %s, %s, %s, %s or %s", kind, AskKindMerge, AskKindHold, + AskKindRestore, AskKindBackup, AskKindRecord) } // HoldAskKey is the key, in AssignerAsksBucket, of the ask about a module's held fingerprint: `hold..`. @@ -81,7 +90,32 @@ func RestoreAskKey(node, module, fingerprint string) (string, error) { return AskKindRestore + "." + node + "." + key, nil } -// ParseAnyAskKey reads an ask key of any kind (MergeAskKey, HoldAskKey, RestoreAskKey); it refuses an unknown kind +// BackupAskKey is the key, in AssignerAsksBucket, of the ask about a module's failed backup on a node before it applies +// a fingerprint: `backup...`. +func BackupAskKey(node, module, fingerprint string) (string, error) { + key, err := ComposedKey(node, module, fingerprint) + if err != nil { + return "", err + } + return AskKindBackup + "." + key, nil +} + +// RecordAskKey is the key, in AssignerAsksBucket, of the ask about a move of a module on a node to the composition +// whose digest it names: `record...`, so a new revision at the same fingerprint is asked +// again (ADR 0539 clause 14). +func RecordAskKey(node, module, composed string) (string, error) { + have, err := HaveKey(node, module) + if err != nil { + return "", err + } + if err := checkDigest(composed, "a record ask's composition digest"); err != nil { + return "", err + } + return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil +} + +// ParseAnyAskKey reads an ask key of any kind (MergeAskKey, HoldAskKey, RestoreAskKey, BackupAskKey, RecordAskKey); it +// refuses an unknown kind // and a key whose parts do not fit its kind, naming the key. func ParseAnyAskKey(key string) (AskKey, error) { kind, rest, _ := strings.Cut(key, ".") @@ -114,6 +148,26 @@ func ParseAnyAskKey(key string) (AskKey, error) { return refused(err) } return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil + case AskKindBackup: + node, module, fp, isNode, err := ParseComposedKey(rest) + if err != nil || isNode { + return refused(fmt.Errorf("a backup ask is backup...")) + } + return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil + case AskKindRecord: + parts := strings.Split(rest, ".") + if len(parts) != 3 { + return refused(fmt.Errorf("a record ask is record...")) + } + node, module, isNode, err := ParseHaveKey(parts[0] + "." + parts[1]) + if err != nil || isNode { + return refused(fmt.Errorf("a record ask is record...")) + } + composed := "sha256:" + parts[2] + if err := checkDigest(composed, "a record ask's composition digest"); err != nil { + return refused(err) + } + return AskKey{Kind: kind, Node: node, Module: module, Composed: composed}, nil } _, err := AskNeedsProof(kind) return refused(err) diff --git a/go/delivery/want_composed_test.go b/go/delivery/want_composed_test.go new file mode 100644 index 0000000..12512e8 --- /dev/null +++ b/go/delivery/want_composed_test.go @@ -0,0 +1,176 @@ +package delivery + +import ( + "strings" + "testing" +) + +func hashOf(c byte) string { return "sha256:" + digestOf(c) } + +// 1943/AC3: a want naming its fingerprint and the digest of the composition it allows round-trips, and so does one +// carrying the answer to a failed backup; a want naming a fingerprint without a digest, a digest not of the sha256 +// form, a removed want naming a digest, an unknown backup answer and a rule past the seventh are refused. +func TestAWantNamesItsComposition(t *testing.T) { + for _, w := range []Want{ + {FP: fp2of('a'), Composed: hashOf('1')}, + {FP: fp2of('a'), Composed: hashOf('1'), Backup: BackupAgain}, + {FP: fp2of('a'), Composed: hashOf('1'), Backup: BackupSkip, Words: "the operator: go on without a backup"}, + {FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: RuleComposed, Words: "no composition of b for ace yet"}, + {Removed: true}, + } { + raw, err := EncodeWant(w) + if err != nil { + t.Fatalf("EncodeWant(%+v): %v", w, err) + } + back, err := DecodeWant(raw) + w.V = Version + if err != nil || !SameWant(back, w) { + t.Errorf("want %+v came back %+v, %v", w, back, err) + } + } + refusals := []struct { + w Want + why string + }{ + {Want{FP: fp2of('a')}, "names no composition"}, + {Want{FP: fp2of('a'), Composed: "sha256:12"}, "not sha256"}, + {Want{Removed: true, Composed: hashOf('1')}, "removed want names no"}, + {Want{FP: fp2of('a'), Composed: hashOf('1'), Backup: "later"}, `backup answer "later"`}, + {Want{Removed: true, Backup: BackupAgain}, "removed want names no"}, + {Want{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 8}, "rule 8"}, + } + for _, r := range refusals { + if _, err := EncodeWant(r.w); err == nil || !strings.Contains(err.Error(), r.why) { + t.Errorf("EncodeWant(%+v) = %v, want a refusal saying %q", r.w, err, r.why) + } + } + if _, err := DecodeWant([]byte(`{"v":1,"fp":"` + fp2of('a') + `"}`)); err == nil || !strings.Contains(err.Error(), "names no composition") { + t.Errorf("a want of release 5, naming no composition, was read: %v", err) + } + if RuleComposed != 7 { + t.Errorf("RuleComposed = %d, want the seventh rule", RuleComposed) + } +} + +// 1943/AC3: the node's own want is `want..node` and round-trips through its parser, while a module named node is +// still refused a want key; its value names the digest of the node's own composition and no fingerprint. +func TestTheNodesOwnWant(t *testing.T) { + key, err := WantNodeKey("ace") + if err != nil || key != "want.ace.node" { + t.Fatalf("WantNodeKey = %q, %v", key, err) + } + if node, err := ParseWantNodeKey(key); err != nil || node != "ace" { + t.Errorf("ParseWantNodeKey(%q) = %q, %v", key, node, err) + } + if _, err := WantKey("ace", "node"); err == nil { + t.Errorf(`WantKey(ace, "node") accepted`) + } + if _, _, err := ParseWantKey(key); err == nil { + t.Errorf("ParseWantKey read the node's own want as a module's") + } + for _, bad := range []string{"want.ace.mesh-mail", "want.node", "ace.node", "want.a.b.node", ""} { + if _, err := ParseWantNodeKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { + t.Errorf("ParseWantNodeKey(%q) = %v, want a refusal naming it", bad, err) + } + } + if _, err := WantNodeKey(""); err == nil { + t.Errorf("WantNodeKey(\"\") accepted") + } + w := NodeWant{Composed: hashOf('2'), Words: "the node's own composition"} + raw, err := EncodeNodeWant(w) + if err != nil { + t.Fatal(err) + } + back, err := DecodeNodeWant(raw) + w.V = Version + if err != nil || back != w { + t.Errorf("node want came back %+v, %v", back, err) + } + for _, bad := range []NodeWant{{}, {Composed: "sha256:1"}} { + if _, err := EncodeNodeWant(bad); err == nil { + t.Errorf("EncodeNodeWant(%+v) accepted", bad) + } + } + if _, err := DecodeNodeWant([]byte(`{"v":1,"composed":"` + hashOf('2') + `","fp":"` + fp2of('a') + `"}`)); err == nil { + t.Errorf("a node want naming a fingerprint was read") + } +} + +// 1943/AC3: a have held after a failed backup, naming the composition it runs, round-trips; so does the node's own have +// naming its composition; a have's digest not of the sha256 form is refused. +func TestAHaveNamesItsComposition(t *testing.T) { + for _, h := range []Have{ + {FP: fp2of('a'), Composed: hashOf('3'), Phase: PhaseHeld, Health: HealthOK, Detail: "the backup failed: no space left"}, + {FP: fp2of('a'), Composed: hashOf('3'), Phase: PhaseRunning, Health: HealthOK}, + } { + raw, err := EncodeHave(h) + if err != nil { + t.Fatalf("EncodeHave(%+v): %v", h, err) + } + back, err := DecodeHave(raw) + h.V = Version + if err != nil || !SameHave(back, h) { + t.Errorf("have %+v came back %+v, %v", h, back, err) + } + } + if _, err := EncodeHave(Have{Composed: "sha256:x", Phase: PhaseRunning, Health: HealthOK}); err == nil || !strings.Contains(err.Error(), "not sha256") { + t.Errorf("a have naming a bad digest = %v", err) + } + n := HaveNode{Composed: hashOf('4')} + raw, err := EncodeHaveNode(n) + if err != nil { + t.Fatal(err) + } + back, err := DecodeHaveNode(raw) + n.V = Version + if err != nil || !SameHaveNode(back, n) { + t.Errorf("node have came back %+v, %v", back, err) + } + if SameHaveNode(n, HaveNode{V: Version, Composed: hashOf('5')}) { + t.Errorf("node haves naming two compositions are the same") + } +} + +// 1943/AC3: the assigner's backup ask (back up again, or go on without one, ADR 0539 clause 16) and its record ask (a +// move a person must allow, clause 14) key by node, module and fingerprint or composition, round-trip through +// ParseAnyAskKey, and the view may answer both. +func TestTheBackupAndRecordAsks(t *testing.T) { + fp, digest := fp2of('a'), hashOf('6') + backup, err := BackupAskKey("ace", "mesh-mail", fp) + if err != nil || backup != "backup.ace.mesh-mail."+strings.Repeat("a", 64) { + t.Fatalf("BackupAskKey = %q, %v", backup, err) + } + record, err := RecordAskKey("ace", "mesh-mail", digest) + if err != nil || record != "record.ace.mesh-mail."+strings.Repeat("6", 64) { + t.Fatalf("RecordAskKey = %q, %v", record, err) + } + if got, err := ParseAnyAskKey(backup); err != nil || got != (AskKey{Kind: AskKindBackup, Node: "ace", Module: "mesh-mail", FP: fp}) { + t.Errorf("ParseAnyAskKey(%q) = %+v, %v", backup, got, err) + } + if got, err := ParseAnyAskKey(record); err != nil || got != (AskKey{Kind: AskKindRecord, Node: "ace", Module: "mesh-mail", Composed: digest}) { + t.Errorf("ParseAnyAskKey(%q) = %+v, %v", record, got, err) + } + for _, kind := range []string{AskKindBackup, AskKindRecord} { + if proof, err := AskNeedsProof(kind); err != nil || proof { + t.Errorf("AskNeedsProof(%s) = %v, %v; the view may answer it", kind, proof, err) + } + } + for _, k := range []string{backup, record} { + if _, err := ViewAnswerKey(k); err != nil { + t.Errorf("the view may not answer %s: %v", k, err) + } + } + for _, bad := range [][3]string{{"", "mesh-mail", fp}, {"ace", "node", fp}, {"ace", "mesh-mail", "src2:1"}} { + if _, err := BackupAskKey(bad[0], bad[1], bad[2]); err == nil { + t.Errorf("BackupAskKey(%v) accepted", bad) + } + } + if _, err := RecordAskKey("ace", "mesh-mail", "sha256:1"); err == nil { + t.Errorf("a record ask naming a bad digest was keyed") + } + for _, bad := range []string{"backup.ace.mesh-mail", "record.ace.mesh-mail.12", "record.ace." + strings.Repeat("6", 64)} { + if _, err := ParseAnyAskKey(bad); err == nil { + t.Errorf("ParseAnyAskKey(%q) accepted", bad) + } + } +}