Compose as the controller does, so a node's shadow can answer same (issue 1126, C1 C2 C3 C5 C6, T1)
Seals are reused and compared by slot (resource, field), never across resources; one rule says which resolved config values are laid; a seat path that steps back is owned by the seat it steps back for, whatever the seat order; a folded module of an old-form build is composed from what runs of it; the operator export's file is carried as it runs; a differing resource names its differing JSON paths, found before its bodies are cut, and an answer is fitted to the node's key.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DifferingPaths is every JSON path at which two bodies of one resource differ, sorted (issue 1126, T1: the shadow
|
||||
// shows its work): object keys and list places joined by dots (`content.url`, `links.0`), a string that holds a JSON
|
||||
// object or list read as that JSON and walked on (a runtime's config carried as text), a key or place one side alone
|
||||
// holds named as it is. A key a secret could be under (secretWords) is named and not walked into. A body that is not
|
||||
// JSON differs at "" (the whole), and a resource one side alone holds has none. At most MaxPaths: the last then says how many more there are. A path names keys
|
||||
// alone, never a value, so it holds no secret.
|
||||
func DifferingPaths(composed, running json.RawMessage) []string {
|
||||
if composed == nil || running == nil {
|
||||
return nil // one side alone holds it: its why says so
|
||||
}
|
||||
var a, b any
|
||||
okA := unmarshalNumbers(composed, &a) == nil
|
||||
okB := unmarshalNumbers(running, &b) == nil
|
||||
if !okA || !okB {
|
||||
if bytes.Equal(composed, running) {
|
||||
return nil
|
||||
}
|
||||
return []string{""}
|
||||
}
|
||||
var out []string
|
||||
walkDiffering(a, b, "", &out)
|
||||
sort.Strings(out)
|
||||
if len(out) > MaxPaths {
|
||||
more := len(out) - (MaxPaths - 1)
|
||||
out = append(out[:MaxPaths-1], "… "+strconv.Itoa(more)+" more")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func unmarshalNumbers(raw []byte, into *any) error {
|
||||
d := json.NewDecoder(bytes.NewReader(raw))
|
||||
d.UseNumber()
|
||||
return d.Decode(into)
|
||||
}
|
||||
|
||||
// asJSON is a string holding a JSON object or list read as that value; v itself otherwise.
|
||||
func asJSON(v any) any {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return v
|
||||
}
|
||||
t := strings.TrimSpace(s)
|
||||
if t == "" || (t[0] != '{' && t[0] != '[') {
|
||||
return v
|
||||
}
|
||||
var parsed any
|
||||
if unmarshalNumbers([]byte(t), &parsed) != nil {
|
||||
return v
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func join(at, key string) string {
|
||||
if at == "" {
|
||||
return key
|
||||
}
|
||||
return at + "." + key
|
||||
}
|
||||
|
||||
func walkDiffering(a, b any, at string, out *[]string) {
|
||||
a, b = asJSON(a), asJSON(b)
|
||||
ma, isMA := a.(map[string]any)
|
||||
mb, isMB := b.(map[string]any)
|
||||
if isMA && isMB {
|
||||
keys := map[string]bool{}
|
||||
for k := range ma {
|
||||
keys[k] = true
|
||||
}
|
||||
for k := range mb {
|
||||
keys[k] = true
|
||||
}
|
||||
for k := range keys {
|
||||
x, inA := ma[k]
|
||||
y, inB := mb[k]
|
||||
switch {
|
||||
case !inA || !inB:
|
||||
*out = append(*out, join(at, k))
|
||||
case secretKey(k):
|
||||
if !sameAny(x, y) {
|
||||
*out = append(*out, join(at, k))
|
||||
}
|
||||
default:
|
||||
walkDiffering(x, y, join(at, k), out)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
la, isLA := a.([]any)
|
||||
lb, isLB := b.([]any)
|
||||
if isLA && isLB {
|
||||
for i := range max(len(la), len(lb)) {
|
||||
if i >= len(la) || i >= len(lb) {
|
||||
*out = append(*out, join(at, strconv.Itoa(i)))
|
||||
continue
|
||||
}
|
||||
walkDiffering(la[i], lb[i], join(at, strconv.Itoa(i)), out)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !sameAny(a, b) {
|
||||
*out = append(*out, at)
|
||||
}
|
||||
}
|
||||
|
||||
func sameAny(a, b any) bool {
|
||||
x, errA := json.Marshal(a)
|
||||
y, errB := json.Marshal(b)
|
||||
return errA == nil && errB == nil && bytes.Equal(x, y)
|
||||
}
|
||||
|
||||
// FitAnswer is a with its shown bodies cut until its JSON is within limit bytes (MaxAnswerBytes for the node's key):
|
||||
// first the bodies' texts, from the last differing module back, keeping their paths; then the paths too. What is cut
|
||||
// is said in the answer's note. The verdict and every list of modules are never cut.
|
||||
func FitAnswer(a CandidateAnswer, limit int) CandidateAnswer {
|
||||
size := func() int {
|
||||
raw, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return len(raw)
|
||||
}
|
||||
if size() <= limit {
|
||||
return a
|
||||
}
|
||||
a.Differs = append([]ModuleDiffers(nil), a.Differs...)
|
||||
for i := range a.Differs {
|
||||
a.Differs[i].Bodies = append([]ResourceBodies(nil), a.Differs[i].Bodies...)
|
||||
}
|
||||
cutTexts, cutPaths := false, false
|
||||
for i := len(a.Differs) - 1; i >= 0 && size() > limit; i-- {
|
||||
for j := range a.Differs[i].Bodies {
|
||||
a.Differs[i].Bodies[j].Composed, a.Differs[i].Bodies[j].Running = "", ""
|
||||
}
|
||||
cutTexts = true
|
||||
}
|
||||
for i := len(a.Differs) - 1; i >= 0 && size() > limit; i-- {
|
||||
for j := range a.Differs[i].Bodies {
|
||||
a.Differs[i].Bodies[j].Paths = nil
|
||||
}
|
||||
cutPaths = true
|
||||
}
|
||||
why := "the bodies shown were cut to keep the answer within " + strconv.Itoa(limit>>10) + " KiB"
|
||||
if cutPaths {
|
||||
why += ", their paths too"
|
||||
}
|
||||
if cutTexts {
|
||||
a.Note = strings.TrimPrefix(a.Note+"; ", "; ") + why
|
||||
}
|
||||
return a
|
||||
}
|
||||
@@ -80,6 +80,9 @@ func CompareComposedWith(composed map[string]Composed, running []Composed, sameV
|
||||
// compareOne is one module's comparison: true when the digests agree, else what differs first.
|
||||
func compareOne(module string, composed, running Composed, sameValue func(a, b string) (bool, bool)) (ModuleDiffers, bool) {
|
||||
composed, running = withoutBusUsers(composed), withoutBusUsers(running)
|
||||
// A sealed value is judged by the value it opens to, slot by slot (issue 1126, C1): a value sealed again at the same
|
||||
// field of the same resource is the box that runs, never a difference; and a box is never matched across resources.
|
||||
composed = KeepSeals(composed, SealsIn(running), sameValue)
|
||||
dc, errC := ComposedDigestOf(composed)
|
||||
dr, errR := ComposedDigestOf(running)
|
||||
if errC != nil {
|
||||
@@ -128,8 +131,11 @@ func compareOne(module string, composed, running Composed, sameValue func(a, b s
|
||||
found = true
|
||||
}
|
||||
if len(bodies) < MaxBodies {
|
||||
// The paths are found on the whole bodies, before either is cut (issue 1126, T1: three resources over
|
||||
// MaxBody could not be told apart).
|
||||
c, r := MaskedPair(x, y, sameValue)
|
||||
bodies = append(bodies, ResourceBodies{ID: id, Why: why, Composed: cut(c), Running: cut(r)})
|
||||
bodies = append(bodies, ResourceBodies{ID: id, Why: why, Paths: DifferingPaths(x, y),
|
||||
Composed: cut(c), Running: cut(r)})
|
||||
}
|
||||
}
|
||||
if found {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// Seals is, by slot, each value one composition carries sealed to its node's key (issue 1126, C1): a file's `sealed`
|
||||
// and a vault box (catalogue.VaultSealedField), as their JSON text (a box is carried as its JSON, a sealed value as
|
||||
// the string it is). A slot is one field of one resource of the composition's module (SealSlot): a box is reused only
|
||||
// at the slot that holds it, never for another resource whose value opens to the same (a provider's grant and its
|
||||
// consumer's secret on one node hold one value).
|
||||
type Seals map[string]string
|
||||
|
||||
// SealedFields are the fields of a resource that carry a value sealed to the node's key.
|
||||
var SealedFields = []string{"sealed", catalogue.VaultSealedField}
|
||||
|
||||
// SealSlot is the slot of one sealed field of one resource, by the resource's id (its place, `#<i>`, when it has none).
|
||||
// The module is the composition's: a slot is never compared across compositions.
|
||||
func SealSlot(resource, field string) string { return resource + "#" + field }
|
||||
|
||||
// SealsIn is every sealed value c carries, by slot; nil when it carries none.
|
||||
func SealsIn(c Composed) Seals {
|
||||
var out Seals
|
||||
for i, raw := range c.Resources {
|
||||
id, fields, ok := sealedFieldsOf(raw, i)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, field := range SealedFields {
|
||||
f, has := fields[field]
|
||||
if !has {
|
||||
continue
|
||||
}
|
||||
if text := sealedText(f); text != "" {
|
||||
if out == nil {
|
||||
out = Seals{}
|
||||
}
|
||||
out[SealSlot(id, field)] = text
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// KeepSeals is c with each sealed value replaced by the one kept at the same slot, where same says the two open to the
|
||||
// same value (known and same): a value the vault sealed again is then the box that runs, so the composition is the
|
||||
// one that runs (ADR 0547 §3, issue 1126). A slot kept holds nothing for, or whose value differs or cannot be opened,
|
||||
// keeps c's. Every other byte of a resource is kept as it is; c itself is not changed.
|
||||
func KeepSeals(c Composed, kept Seals, same func(a, b string) (same, known bool)) Composed {
|
||||
if len(kept) == 0 || same == nil {
|
||||
return c
|
||||
}
|
||||
out := c
|
||||
out.Resources = nil
|
||||
for i, raw := range c.Resources {
|
||||
id, fields, ok := sealedFieldsOf(raw, i)
|
||||
if ok {
|
||||
for _, field := range SealedFields {
|
||||
f, has := fields[field]
|
||||
if !has {
|
||||
continue
|
||||
}
|
||||
ours := sealedText(f)
|
||||
theirs, held := kept[SealSlot(id, field)]
|
||||
if !held || ours == "" || theirs == ours {
|
||||
continue
|
||||
}
|
||||
if s, known := same(ours, theirs); !known || !s {
|
||||
continue
|
||||
}
|
||||
value := json.RawMessage(theirs)
|
||||
if f[0] == '"' {
|
||||
value, _ = json.Marshal(theirs)
|
||||
}
|
||||
if swapped, ok := withField(raw, field, value); ok {
|
||||
raw = swapped
|
||||
}
|
||||
}
|
||||
}
|
||||
out.Resources = append(out.Resources, raw)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sealedFieldsOf is a resource's id (`#<i>` when it has none) and its top-level fields, false when it is no object.
|
||||
func sealedFieldsOf(raw json.RawMessage, i int) (string, map[string]json.RawMessage, bool) {
|
||||
var fields map[string]json.RawMessage
|
||||
if json.Unmarshal(raw, &fields) != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
var id string
|
||||
if f, ok := fields["id"]; !ok || json.Unmarshal(f, &id) != nil || id == "" {
|
||||
id = "#" + strconv.Itoa(i)
|
||||
}
|
||||
return id, fields, true
|
||||
}
|
||||
|
||||
// sealedText is a sealed field as its text: a string as it is, a box as its JSON.
|
||||
func sealedText(f json.RawMessage) string {
|
||||
var text string
|
||||
if json.Unmarshal(f, &text) == nil {
|
||||
return text
|
||||
}
|
||||
var compact bytes.Buffer
|
||||
if json.Compact(&compact, f) != nil {
|
||||
return string(f)
|
||||
}
|
||||
return compact.String()
|
||||
}
|
||||
|
||||
// withField is the object raw with its top-level field replaced by value, every other byte as it was (so a composition
|
||||
// whose sealed values are those that run is the one that runs, byte for byte); false when raw is no object holding it.
|
||||
func withField(raw json.RawMessage, field string, value json.RawMessage) (json.RawMessage, bool) {
|
||||
d := json.NewDecoder(bytes.NewReader(raw))
|
||||
if t, err := d.Token(); err != nil || t != json.Delim('{') {
|
||||
return nil, false
|
||||
}
|
||||
for d.More() {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
key, _ := t.(string)
|
||||
before := d.InputOffset()
|
||||
var skip json.RawMessage
|
||||
if err := d.Decode(&skip); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
if key != field {
|
||||
continue
|
||||
}
|
||||
after := d.InputOffset()
|
||||
// The span holds the colon, any space and the value: kept up to the colon, the value replaced.
|
||||
span := raw[before:after]
|
||||
colon := bytes.IndexByte(span, ':')
|
||||
if colon < 0 {
|
||||
return nil, false
|
||||
}
|
||||
out := make([]byte, 0, len(raw)-len(span)+colon+1+len(value))
|
||||
out = append(out, raw[:int(before)+colon+1]...)
|
||||
out = append(out, value...)
|
||||
out = append(out, raw[after:]...)
|
||||
return out, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
+10
-2
@@ -48,6 +48,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
@@ -355,6 +356,10 @@ type Have struct {
|
||||
// after a restart while it opens to the vault's value, so a value sealed again moves no composition. Openable by the
|
||||
// node alone, as the vault hands them over the bus. Empty on every other have.
|
||||
Sealed []string `json:"sealed,omitempty"`
|
||||
// SealedAt is Sealed by slot (Seals: a field of a resource, issue 1126, C1): the composer reuses a box only at the
|
||||
// slot that holds it, so a provider and a consumer of one value on one node each keep their own. Written in Sealed's
|
||||
// place by a node-engine that knows it; Sealed is read only from a have written before.
|
||||
SealedAt Seals `json:"sealed-at,omitempty"`
|
||||
}
|
||||
|
||||
// UserListWritten is the bus's user list as the node-engine of the bus node wrote it to the bus module's file, applied
|
||||
@@ -406,6 +411,8 @@ type HaveNode struct {
|
||||
// applied (Have.Sealed for the node's own, issue 1126): a value the vault seals again after a restart moves no
|
||||
// composition, so a restart applies nothing again.
|
||||
Sealed []string `json:"sealed,omitempty"`
|
||||
// SealedAt is Sealed by slot (Have.SealedAt).
|
||||
SealedAt Seals `json:"sealed-at,omitempty"`
|
||||
// Candidates is the node's answer to each placement candidate naming it, by group (PlacementCandidate): written by
|
||||
// its shadow composer whenever an answer changes, removed when the candidate key goes. The node applies nothing of a
|
||||
// candidate.
|
||||
@@ -586,10 +593,11 @@ func DecodePulse(data []byte) (Pulse, error) {
|
||||
func SameHave(a, b Have) bool {
|
||||
ua, ub := a.UserList, b.UserList
|
||||
a.UserList, b.UserList = nil, nil
|
||||
if !slices.Equal(a.Sealed, b.Sealed) {
|
||||
if !slices.Equal(a.Sealed, b.Sealed) || !maps.Equal(a.SealedAt, b.SealedAt) {
|
||||
return false
|
||||
}
|
||||
a.Sealed, b.Sealed = nil, nil
|
||||
a.SealedAt, b.SealedAt = nil, nil
|
||||
if !reflect.DeepEqual(a, b) || (ua == nil) != (ub == nil) {
|
||||
return false
|
||||
}
|
||||
@@ -607,7 +615,7 @@ func SameHaveNode(a, b HaveNode) bool {
|
||||
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) &&
|
||||
(a.Record == nil) == (b.Record == nil) && (a.Record == nil || SameNodeRecord(*a.Record, *b.Record)) &&
|
||||
jsonEqual(a.Sealed, b.Sealed) && jsonEqual(a.Candidates, b.Candidates) && a.Composes == b.Composes &&
|
||||
jsonEqual(a.Sealed, b.Sealed) && maps.Equal(a.SealedAt, b.SealedAt) && jsonEqual(a.Candidates, b.Candidates) && a.Composes == b.Composes &&
|
||||
a.ComposesWhy == b.ComposesWhy
|
||||
}
|
||||
|
||||
|
||||
@@ -163,16 +163,24 @@ type ModuleDiffers struct {
|
||||
// ResourceBodies is one differing resource of a module: its id, why, and its body composed and as it runs, masked
|
||||
// (MaskedPair); a side that does not hold it is empty.
|
||||
type ResourceBodies struct {
|
||||
ID string `json:"id"`
|
||||
Why string `json:"why"`
|
||||
Composed string `json:"composed,omitempty"`
|
||||
Running string `json:"running,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Why string `json:"why"`
|
||||
// Paths is every JSON path at which the two bodies differ (DifferingPaths: `sealed`, `content.url`, `links.0`), found
|
||||
// on the whole bodies before they are cut, a JSON value written in a string read as JSON; a path names keys alone,
|
||||
// never a value. At most MaxPaths, the last then saying how many more.
|
||||
Paths []string `json:"paths,omitempty"`
|
||||
Composed string `json:"composed,omitempty"`
|
||||
Running string `json:"running,omitempty"`
|
||||
}
|
||||
|
||||
// The bounds of a module's shown bodies.
|
||||
// The bounds of a module's shown bodies, and of one answer as the node's own key holds it: an answer over
|
||||
// MaxAnswerBytes is fitted (FitAnswer). The node's key (HaveBucket) takes a value of about 1 MiB
|
||||
// (buscompose.ChainValueBytes) and holds every group's answer beside the node's facts, so one answer keeps to a sixth.
|
||||
const (
|
||||
MaxBodies = 20
|
||||
MaxBody = 8192
|
||||
MaxBodies = 20
|
||||
MaxBody = 8192
|
||||
MaxPaths = 64
|
||||
MaxAnswerBytes = 160 << 10
|
||||
)
|
||||
|
||||
// ModuleAdded is one module the candidate places on the node and the node does not run: its composition's digest when
|
||||
|
||||
@@ -3,7 +3,9 @@ package delivery
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -288,16 +290,30 @@ func TestPlacement(t *testing.T) {
|
||||
sealedFile := func(id, sealed, mode string) json.RawMessage {
|
||||
return json.RawMessage(`{"id":"` + id + `","type":"file","module":"nats","path":"/etc/` + id + `","mode":"` + mode + `","sealed":"` + sealed + `"}`)
|
||||
}
|
||||
shown := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("a", "box-1", "0600"), sealedFile("b", "box-2", "0600"), file("c", "x"))},
|
||||
[]Composed{comp(sealedFile("a", "box-1", "0644"), sealedFile("b", "box-3", "0600"), file("c", "y"))},
|
||||
func(a, b string) (same, known bool) { return a == "box-2" && b == "box-3", true })
|
||||
if len(shown.Differs) != 1 || len(shown.Differs[0].Bodies) != 3 {
|
||||
t.Fatalf("every differing resource is not shown: %+v", shown.Differs)
|
||||
// Issue 1126, C1: a sealed value is judged by the value it opens to at its own slot: b sealed again (box-2 and
|
||||
// box-3 open to one value) is no difference; d holds at its slot a box of another value, though that value is
|
||||
// the one b holds, and differs at its path `sealed`.
|
||||
sameBox := func(a, b string) (same, known bool) {
|
||||
return (a == "box-2" && b == "box-3") || a == b, true
|
||||
}
|
||||
for _, row := range []struct{ id, has, hasNot string }{
|
||||
{"a", `"mode":"0600"`, "box-1"},
|
||||
{"b", "another seal of the same value", "box-2"},
|
||||
{"c", `"content":"x"`, ""},
|
||||
shown := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("a", "box-1", "0600"), sealedFile("b", "box-2", "0600"),
|
||||
file("c", "x"), sealedFile("d", "box-2", "0600"))},
|
||||
[]Composed{comp(sealedFile("a", "box-1", "0644"), sealedFile("b", "box-3", "0600"), file("c", "y"), sealedFile("d", "box-4", "0600"))},
|
||||
sameBox)
|
||||
if len(shown.Differs) != 1 || len(shown.Differs[0].Bodies) != 3 {
|
||||
t.Fatalf("every differing resource is not shown, or b sealed again is shown: %+v", shown.Differs)
|
||||
}
|
||||
if c := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("b", "box-2", "0600"))},
|
||||
[]Composed{comp(sealedFile("b", "box-3", "0600"))}, sameBox); len(c.Same) != 1 {
|
||||
t.Errorf("a value sealed again at its own slot is not the same: %+v", c)
|
||||
}
|
||||
for _, row := range []struct {
|
||||
id, has, hasNot string
|
||||
paths []string
|
||||
}{
|
||||
{"a", `"mode":"0600"`, "box-1", []string{"mode"}},
|
||||
{"c", `"content":"x"`, "", []string{"content"}},
|
||||
{"d", "<masked: another value>", "box-2", []string{"sealed"}},
|
||||
} {
|
||||
var body ResourceBodies
|
||||
for _, b := range shown.Differs[0].Bodies {
|
||||
@@ -306,10 +322,32 @@ func TestPlacement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
both := body.Composed + body.Running
|
||||
if !strings.Contains(both, row.has) || (row.hasNot != "" && strings.Contains(both, row.hasNot)) {
|
||||
t.Errorf("%s shown as %+v", row.id, body)
|
||||
if !strings.Contains(both, row.has) || (row.hasNot != "" && strings.Contains(both, row.hasNot)) ||
|
||||
!slices.Equal(body.Paths, row.paths) {
|
||||
t.Errorf("%s shown as %+v, want paths %v", row.id, body, row.paths)
|
||||
}
|
||||
}
|
||||
// Issue 1126, T1: the paths are found on the whole bodies, before they are cut; JSON in a string is walked into, a
|
||||
// key a secret could be under is named and never shown.
|
||||
long := strings.Repeat("x", MaxBody)
|
||||
conf := func(link, password string) json.RawMessage {
|
||||
inner, _ := json.Marshal(map[string]any{"pad": long, "links": []string{"a", link}, "password": password})
|
||||
outer, _ := json.Marshal(map[string]any{"id": "conf", "type": "file", "module": "nats", "path": "/etc/conf",
|
||||
"mode": "0600", "content": string(inner)})
|
||||
return outer
|
||||
}
|
||||
far := CompareComposedWith(map[string]Composed{"k": comp(conf("b", "hunter2"))}, []Composed{comp(conf("c", "hunter3"))}, nil)
|
||||
if len(far.Differs) != 1 || len(far.Differs[0].Bodies) != 1 ||
|
||||
!slices.Equal(far.Differs[0].Bodies[0].Paths, []string{"content.links.1", "content.password"}) ||
|
||||
!strings.Contains(far.Differs[0].Bodies[0].Composed, "cut at") || strings.Contains(fmt.Sprint(far), "hunter") {
|
||||
t.Errorf("a difference past the cut: %+v", far.Differs)
|
||||
}
|
||||
// And an answer too large for the node's key is fitted: the texts go first, the verdict and the paths stay.
|
||||
big := CandidateAnswer{Head: "h", Verdict: CandidateDiffers, Differs: far.Differs}
|
||||
if fitted := FitAnswer(big, 4<<10); len(fitted.Differs[0].Bodies[0].Composed) != 0 || len(fitted.Differs[0].Bodies[0].Paths) != 2 ||
|
||||
fitted.Verdict != CandidateDiffers || !strings.Contains(fitted.Note, "cut") || big.Differs[0].Bodies[0].Composed == "" {
|
||||
t.Errorf("fitted as %+v", fitted)
|
||||
}
|
||||
if c, r := MaskedPair(json.RawMessage(`{"env":{"DB_PASSWORD":"hunter2","PORT":"80"},"list":[{"token":"t1"}]}`),
|
||||
json.RawMessage(`{"env":{"DB_PASSWORD":"hunter3","PORT":"81"},"list":[{"token":"t1"}]}`), nil); strings.Contains(c+r, "hunter") ||
|
||||
strings.Contains(c+r, `"t1"`) || !strings.Contains(r, `"PORT":"81"`) || !strings.Contains(c, "<masked: differs>") ||
|
||||
|
||||
Reference in New Issue
Block a user