package ownresync import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/delivery" ) // Issue 1126, cause C4: after a shadow answer saying the vault holds another value of an own secret than runs, the node // hands the vault what runs, in the verb's two steps, once per running value; and only while it applies the // controller's compositions and runs the one its want names. func TestTheNodeHandsTheVaultWhatRunsOfADriftedOwnSecret(t *testing.T) { const running = "the-value-that-runs" digest := "sha256:" + strings.Repeat("a", 64) fp := "src2:" + strings.Repeat("b", 64) sum := sha256.Sum256([]byte(running)) answers := map[string]delivery.CandidateAnswer{"g": {Proposal: "p-1", Revision: 1, Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{Module: "only-office", Why: "the resource differs", Bodies: []delivery.ResourceBodies{ {ID: "only-office.needs-jwt", Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`}, {ID: "only-office.needs-salt", Composed: `{"sealed":""}`}}}}}} for name, row := range map[string]struct { composes string runs string // the have's composition digest refuse string // the vault's refusal asks int // the vault's verb asked, both passes says string // Issue 1126, 2026-10-28: a read or an ask the bus did not answer is no answer: taken again (Retry), never kept. wantRead string // the want's read: "" answers it, "fails" the bus does not answer, "absent" no key retry bool }{ "applies the controller's, runs its want": {delivery.ComposesController, digest, "", 2, "generation 1 replaced by 2", "", false}, "composes its own": {delivery.ComposesOwn, digest, "", 0, "composes its own", "", false}, "not decided yet": {"", digest, "", 0, "", "", false}, "runs not yet what its want names": {delivery.ComposesController, "sha256:" + strings.Repeat("c", 64), "", 0, "not yet the composition", "", false}, "the vault refuses": {delivery.ComposesController, digest, "switched off", 1, "switched off", "", false}, "the vault does not answer": {delivery.ComposesController, digest, "nats: no responders available for request", 2, "asked again", "", true}, "the want's read does not answer": {delivery.ComposesController, digest, "", 0, "asked again", "fails", true}, "it has no want": {delivery.ComposesController, digest, "", 0, "it has no want", "absent", false}, } { var asked []map[string]any var said []string want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Composed: digest}) have, _ := delivery.EncodeHave(delivery.Have{FP: fp, Composed: row.runs, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) r := New(Env{Node: "novox", Composes: func() string { return row.composes }, Running: func() []delivery.Composed { return []delivery.Composed{{Node: "novox", Module: "only-office", FP: fp, Resources: []json.RawMessage{ // As novox runs it (its shadow answer, 2026-10-28): the id carries the module's name. json.RawMessage(`{"id":"only-office.needs-jwt","module":"only-office","type":"file",` + `"path":"/var/lib/only-office/jwt.secret","sealed":"box-of-the-running-value"}`)}}} }, Get: func(_ context.Context, bucket, key string) ([]byte, error) { switch { case bucket == delivery.AssignerWantsBucket && key == "want.novox.only-office" && row.wantRead == "fails": return nil, errors.New("the bucket mesh-delivery-assigner_wants: nats: no responders available for request") case bucket == delivery.AssignerWantsBucket && key == "want.novox.only-office" && row.wantRead == "absent": return nil, nil case bucket == delivery.AssignerWantsBucket && key == "want.novox.only-office": return want, nil case bucket == delivery.HaveBucket && key == "novox.only-office": return have, nil } return nil, nil }, Unseal: func(sealed string) ([]byte, error) { if sealed != "box-of-the-running-value" { return nil, errors.New("not this machine's") } return []byte(running), nil }, Seal: func(to string, value []byte) (string, error) { return to + "(" + string(value) + ")", nil }, Ask: func(_ context.Context, verb string, args map[string]any) (json.RawMessage, error) { asked = append(asked, args) if verb != "move-in" || args["replace_running"] != true { t.Errorf("%s: asked %s with %v, not move-in with replace_running", name, verb, args) } if row.refuse != "" { return nil, errors.New(row.refuse) } if args["call"] == nil { if args["module"] != "only-office" || args["name"] != "jwt" || args["node"] != nil { t.Errorf("%s: the first step names %v", name, args) } return json.RawMessage(`{"call":"c1","seal_to":"key","secret":"own/novox/only-office/jwt"}`), nil } if args["sealed"] != "key("+running+")" { t.Errorf("%s: handed %v, not what runs sealed to the call's key", name, args["sealed"]) } return json.RawMessage(`{"secret":"own/novox/only-office/jwt","from_generation":1,"to_generation":2,` + `"fingerprint":"sha256:` + hex.EncodeToString(sum[:]) + `"}`), nil }, Say: func(s string) { said = append(said, s) }}) first := r.Take(context.Background(), answers) again := r.Take(context.Background(), answers) if len(asked) != row.asks { t.Errorf("%s: the vault asked %d times, want %d: %v", name, len(asked), row.asks, asked) } if retry := len(first) == 1 && first[0].Retry; retry != row.retry { t.Errorf("%s: taken again %v, want %v: %+v", name, retry, row.retry, first) } if row.retry && (len(again) != 1 || !again[0].Retry || again[0].Said != "") || !row.retry && len(again) != 0 { t.Errorf("%s: the same answer did or said something again: %+v", name, again) } all := strings.Join(said, "\n") if row.says == "" && (len(first) != 0 || all != "") || row.says != "" && (len(first) != 1 || !strings.Contains(all, row.says)) { t.Errorf("%s: did %+v and said %q, want %q", name, first, all, row.says) } if strings.Contains(all, running) { t.Errorf("%s: the value is in what the node said", name) } } }