package resolution import ( "reflect" "strings" "testing" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) const handedBot = `{"module":"bot","version":"1", "own-secrets":{"token":"${dir:state}/token"}, "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[ {"id":"state","type":"directory","mode":"0700","place":"."}, {"id":"conf","type":"file","path":"/etc/bot/bot.conf","mode":"0640","template":"files/bot.conf"}, {"id":"daemon","type":"process","name":"bot","run":["bot"],"env":{"BOT_TOKEN_FILE":"${dir:state}/token"}}]}` const botText = "token = ${secret:token}\nhome = ${machine:account-home}\n" func botMoved() string { return strings.Replace(handedBot, `"version":"1",`, `"version":"1","moved":true,`, 1) } func builtBot(text string) []mcat.Built { return []mcat.Built{{Name: "files", Kind: mcat.ArtifactArchive, Reference: mcat.ArtifactStoreScheme + "bot/files/blobs/sha256:" + strings.Repeat("e", 64), Digest: "sha256:" + strings.Repeat("e", 64), Texts: map[string]string{"bot.conf": text}}} } // What the controller seals for the bot when it fills it the old way: a stand-in sealed value. func sealedForBot(with mcat.Rendering) mcat.Rendering { with.Needed = map[string]map[string]string{"bot": {"token": "sealed-to-the-node"}} return with } func TestAHandedModulesSecretsAreNamedAndNoValueIsSealed(t *testing.T) { r := notifierOn(t, held(t, botMoved(), builtBot(botText))) c, got := composeWith(t, r, handingTo("store:5000")) if strings.Contains(got, "sealed") || strings.Contains(got, "${secret:") { t.Fatalf("a handed module's declaration carries a sealed value or a secret placeholder: %s", got) } var token map[string]any for _, res := range c.Resources { if res["id"] == "bot.needs-token" { token = res } } if token == nil { t.Fatalf("the token's file is not declared: %s", got) } if token["path"] == nil || strings.Contains(token["path"].(string), "${") || token["mode"] != "0600" || token["owner-at-runtime"] != "owned" || token[mcat.HandedSecretField] != nil { t.Fatalf("the token's file is declared %v: placed, 0600, owned at runtime, no value", token) } path := token["path"].(string) if !reflect.DeepEqual(c.Filled, []string{path, "/etc/bot/bot.conf"}) { t.Fatalf("the filled files are %v", c.Filled) } if want := []mcat.PlanSecret{{Name: "token", Path: path}}; !reflect.DeepEqual(c.Plans["bot"].Secrets, want) { t.Fatalf("the plan names secrets %+v, want %+v", c.Plans["bot"].Secrets, want) } // Issue 774 (postgres on novox, 2026-10-31): where the vault judged it handed, its secrets are the vault's even on a // machine the node reads as not taking a filled file, and nothing is refused for a value the vault never answers. judged, err := Compose(r, mcat.Rendering{ArtifactStore: "store:5000", Assigned: map[string]bool{"bot": true}}) if err != nil { t.Fatalf("a module the vault judged handed is refused: %v", err) } if want := []mcat.PlanSecret{{Name: "token", Path: path}}; !reflect.DeepEqual(judged.Plans["bot"].Secrets, want) { t.Errorf("judged handed by the vault, the plan names secrets %+v, want %+v", judged.Plans["bot"].Secrets, want) } // A process naming the token's file still restarts when its client writes it. for _, res := range c.Resources { if res["id"] == "bot.daemon" && !strings.Contains(strings.Join(stringsIn(res["restart-on"]), " "), "bot.needs-token") { t.Errorf("the daemon does not restart on its token's file: %v", res["restart-on"]) } } }