Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)
This commit is contained in:
@@ -48,7 +48,7 @@ func tools() []stdio.Tool {
|
||||
Description: fmt.Sprintf("Ask the operator for a value in a prompt that shows nothing of what is typed, "+
|
||||
"and answer it sealed to the key seal_to names — never in the clear (novox/hq ADR 0259 §10): how the "+
|
||||
"mesh takes a module's own secret at the desk. The prompt is written here from the names, and says the "+
|
||||
"mesh's controller asks: the bus lets nobody else ask it. Answers sealed, or cancelled when the prompt was "+
|
||||
"mesh's controller or vault asks: the bus lets nobody else ask it. Answers sealed, or cancelled when the prompt was "+
|
||||
"dismissed, answered empty or not answered within timeout_seconds (default and at most %d).", menuMost),
|
||||
Input: map[string]any{
|
||||
"type": "object",
|
||||
@@ -62,7 +62,7 @@ func tools() []stdio.Tool {
|
||||
"required": []string{"module", "secret", "node", "seal_to"},
|
||||
},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
q, err := secretQuestion(args)
|
||||
q, err := secretQuestion(stdio.CallerOf(args), args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
type manifest struct {
|
||||
Module string `json:"module"`
|
||||
Moved bool `json:"moved"`
|
||||
Version string `json:"version"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
Requires []string `json:"requires"`
|
||||
@@ -23,7 +24,8 @@ type manifest struct {
|
||||
Shell []shellCode `json:"shell"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
// Lines for other modules' seats (novox/hq ADR 0212): the window manager's, here.
|
||||
Contributions []contribution `json:"contributions"`
|
||||
Items []item `json:"items"`
|
||||
Contributions []any `json:"contributions"`
|
||||
Build struct {
|
||||
Artifacts []map[string]any `json:"artifacts"`
|
||||
} `json:"build"`
|
||||
@@ -85,26 +87,66 @@ func (m manifest) packages() (present, absent []string) {
|
||||
return present, absent
|
||||
}
|
||||
|
||||
// sameAsSource checks that a file resource's content is byte for byte the module's source file, so
|
||||
// the readable file in the repository is what the machine gets.
|
||||
func (m manifest) sameAsSource(t *testing.T, id, source string) {
|
||||
// fromTemplate checks that a file resource takes its text from its template file in the module's
|
||||
// repository (novox/hq ADR 0349 §2): it names the template, carries no text of its own beside it, and the
|
||||
// template is there. The readable file in the repository is the one place the machine's text is written.
|
||||
func (m manifest) fromTemplate(t *testing.T, id, template string) {
|
||||
t.Helper()
|
||||
want, err := os.ReadFile(filepath.Join("..", "..", source))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := m.resource(t, id)
|
||||
if r["type"] != "file" {
|
||||
t.Fatalf("%s is a %v, not a file", id, r["type"])
|
||||
}
|
||||
if got, _ := r["content"].(string); got != string(want) {
|
||||
t.Fatalf("resource %s's content is not %s: edit the source and copy it into module.json", id, source)
|
||||
if r["template"] != template {
|
||||
t.Fatalf("resource %s does not take its text from %s: template %v", id, template, r["template"])
|
||||
}
|
||||
if _, inline := r["content"]; inline {
|
||||
t.Fatalf("resource %s keeps its text inline beside its template: a file's text is in one place", id)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join("..", "..", template)); err != nil {
|
||||
t.Fatalf("resource %s names %s: %v", id, template, err)
|
||||
}
|
||||
if r["owner"] != "${machine:account}" && !strings.HasPrefix(r["path"].(string), "/etc/") {
|
||||
t.Fatalf("%s under the home is the account's", id)
|
||||
}
|
||||
}
|
||||
|
||||
// everyFileIsATemplate checks that no file resource carries its text inline, and that the build packs
|
||||
// files/ into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
|
||||
func (m manifest) everyFileIsATemplate(t *testing.T) {
|
||||
t.Helper()
|
||||
for _, r := range m.Resources {
|
||||
if _, inline := r["content"]; inline {
|
||||
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
|
||||
}
|
||||
}
|
||||
packs := 0
|
||||
for _, a := range m.Build.Artifacts {
|
||||
if a["kind"] == "archive" && a["from"] == "files" {
|
||||
packs++
|
||||
if a["name"] != "files" {
|
||||
t.Errorf("the templates archive is named %v, not files", a["name"])
|
||||
}
|
||||
}
|
||||
}
|
||||
if packs != 1 {
|
||||
t.Errorf("the build packs files/ into %d archives, not one", packs)
|
||||
}
|
||||
}
|
||||
|
||||
// text is the text a file resource places on the machine: its template file's.
|
||||
func (m manifest) text(t *testing.T, id string) string {
|
||||
t.Helper()
|
||||
name, _ := m.resource(t, id)["template"].(string)
|
||||
if name == "" {
|
||||
t.Fatalf("resource %s names no template", id)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// checkTheToolsAgree checks that the manifest lists the module's own tools exactly, that the bundle
|
||||
// serves each seat verb the claims promise as <seat>.<verb>, and that the Go bundle is declared.
|
||||
func checkTheToolsAgree(t *testing.T, m manifest) {
|
||||
@@ -176,31 +218,49 @@ func checkNoSecretsOrInstallationNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type contribution struct {
|
||||
Seat string `json:"seat"`
|
||||
Kind string `json:"kind"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
// displaySession is the collection the module's lines for the window manager are an item of.
|
||||
const displaySession = "contribution:node-display-session:config"
|
||||
|
||||
// i3Lines is what the module contributes to the window manager.
|
||||
// i3Lines is what the module contributes to the window manager: the text of its item of the collection
|
||||
// contribution:node-display-session:config (design 57 L3, issue 848).
|
||||
func (m manifest) i3Lines() string {
|
||||
var out string
|
||||
for _, c := range m.Contributions {
|
||||
if c.Seat == "node-display-session" && c.Kind == "config" {
|
||||
out += c.Content
|
||||
for _, it := range m.Items {
|
||||
if it.Collection == displaySession {
|
||||
text, err := os.ReadFile(filepath.Join("..", "..", it.Template))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
out += string(text)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// i3LinesAreSource checks the window-manager contribution is the source file it is written from.
|
||||
// i3LinesAreSource checks the window-manager lines are one item whose template is the source file, and that no
|
||||
// old-form contribution to node-display-session is left.
|
||||
func (m manifest) i3LinesAreSource(t *testing.T, source string) {
|
||||
t.Helper()
|
||||
want, err := os.ReadFile(filepath.Join("..", "..", source))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if len(m.Contributions) != 0 {
|
||||
t.Fatalf("the module still holds contributions: the lines are an item of %s", displaySession)
|
||||
}
|
||||
if m.i3Lines() != string(want) {
|
||||
t.Fatalf("the contribution to node-display-session is not %s: edit the source and copy it into module.json", source)
|
||||
var templates []string
|
||||
for _, it := range m.Items {
|
||||
if it.Collection == displaySession {
|
||||
templates = append(templates, it.Template)
|
||||
}
|
||||
}
|
||||
if len(templates) != 1 || templates[0] != source {
|
||||
t.Fatalf("the items of %s are %v, want the one template %s", displaySession, templates, source)
|
||||
}
|
||||
if m.i3Lines() == "" {
|
||||
t.Fatalf("the template %s holds no line", source)
|
||||
}
|
||||
}
|
||||
|
||||
type item struct {
|
||||
Collection string `json:"collection"`
|
||||
Name string `json:"name"`
|
||||
Attributes map[string]string `json:"attributes"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestItOwnsItsFilesAsWrittenInTheModule(t *testing.T) {
|
||||
"launch": "files/bin/rofi-launch",
|
||||
"powermenu": "files/bin/rofi-powermenu",
|
||||
} {
|
||||
m.sameAsSource(t, id, source)
|
||||
m.fromTemplate(t, id, source)
|
||||
}
|
||||
for _, id := range []string{"dmenu", "launch", "powermenu"} {
|
||||
if m.resource(t, id)["mode"] != "0755" {
|
||||
@@ -51,25 +51,25 @@ func TestItOwnsItsFilesAsWrittenInTheModule(t *testing.T) {
|
||||
|
||||
func TestTheThemesAreTheMeshsFacesAndTheConfigurationNamesOne(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
conf := m.resource(t, "configuration")["content"].(string)
|
||||
conf := m.text(t, "configuration")
|
||||
if !strings.Contains(conf, `@theme "mesh"`) || strings.Contains(conf, "@import") {
|
||||
t.Fatal("the configuration names the theme mesh and imports nothing")
|
||||
}
|
||||
for _, id := range []string{"theme", "theme-powermenu"} {
|
||||
c := m.resource(t, id)["content"].(string)
|
||||
c := m.text(t, id)
|
||||
if !strings.Contains(c, `"JetBrainsMono Nerd Font`) || strings.Contains(c, "@import") ||
|
||||
strings.Contains(c, "Hack") || strings.Contains(c, "Iosevka") {
|
||||
t.Errorf("%s: the monospace face, and no imported file", id)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(m.resource(t, "theme")["content"].(string), `"Inter 11"`) {
|
||||
if !strings.Contains(m.text(t, "theme"), `"Inter 11"`) {
|
||||
t.Error("the theme's prose is in the interface face")
|
||||
}
|
||||
}
|
||||
|
||||
func TestThePowerMenuNamesNoWindowManagerAndNoLocker(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
menu := m.resource(t, "powermenu")["content"].(string)
|
||||
menu := m.text(t, "powermenu")
|
||||
for _, never := range []string{"i3-msg", "i3lock", "betterlockscreen", "openbox", "bspc", "qdbus", "mpc"} {
|
||||
if strings.Contains(menu, never) {
|
||||
t.Errorf("the power menu names %s", never)
|
||||
@@ -109,3 +109,8 @@ func TestTheToolsAgreeWithTheManifest(t *testing.T) {
|
||||
checkTheToolsAgree(t, m)
|
||||
checkNoSecretsOrInstallationNames(t)
|
||||
}
|
||||
|
||||
// No file resource carries its text inline (novox/hq ADR 0349 §2).
|
||||
func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
|
||||
readManifest(t).everyFileIsATemplate(t)
|
||||
}
|
||||
|
||||
@@ -14,12 +14,13 @@ import (
|
||||
|
||||
// node-launcher's verb `secret` (novox/hq ADR 0259 §10): a prompt that does not show what is typed, whose
|
||||
// answer is sealed to the key the asker gives before it leaves this process — never in the clear on the
|
||||
// bus, in the runtime's log or in any call's record. The controller asks it to take a module's own secret,
|
||||
// such as a bot token, from the operator at their desk.
|
||||
// bus, in the runtime's log or in any call's record. The vault asks it, in its own name, to take a module's own
|
||||
// secret, such as a bot token, from the operator at their desk; the controller asks it for a module still filled
|
||||
// the old way, until R1 (issue 682).
|
||||
//
|
||||
// **What it does not do**: on an X11 desk any program of the operator's account can read the keys as they
|
||||
// are typed, and a program that asks this verb with a key of its own is answered with what the operator
|
||||
// typed into a prompt they did not ask for. The bus lets only the controller ask this verb, and the prompt,
|
||||
// typed into a prompt they did not ask for. The bus lets only the controller and the vault ask this verb, and the prompt,
|
||||
// written here from names, says so and for what, so the operator types only into one they started with give
|
||||
// (ADR 0234's accepted residual risk is what remains: a program drawing a window of its own).
|
||||
|
||||
@@ -46,11 +47,22 @@ var aName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
|
||||
// break, no markup, no punctuation a sentence is written with — so the prompt's words stay this holder's.
|
||||
var aCaller = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/@ -]{0,79}$`)
|
||||
|
||||
// theVault says the caller the runtime proved is the vault's own account (`<node>.mesh-vault`, issue 723): the one
|
||||
// account beside the controller the bus lets ask this verb, and the only one that asks it in its own name.
|
||||
func theVault(caller string) bool { return strings.HasSuffix(caller, ".mesh-vault") }
|
||||
|
||||
// escapeMarkup is `what` shown as text, never markup (novox/hq issue 789): the window's message is read with
|
||||
// pango markup on, so a catalogue's words reach it with the characters pango gives meaning to turned to their
|
||||
// entities first — never interpreted, whatever a module's manifest holds.
|
||||
var escapeMarkupReplacer = strings.NewReplacer("&", "&", "<", "<", ">", ">")
|
||||
|
||||
func escapeMarkup(s string) string { return escapeMarkupReplacer.Replace(s) }
|
||||
|
||||
// secretQuestion reads the call. The prompt is this holder's words, written from the module, the secret and the
|
||||
// machine (the review of 2026-10-09, M4): it says the mesh's controller asks, which the bus makes true — it
|
||||
// denies these subjects to every principal but the controller — and a caller's own words are refused, so
|
||||
// nobody puts a prompt of its choosing before the operator.
|
||||
func secretQuestion(args map[string]any) (SecretQuestion, error) {
|
||||
// machine (the review of 2026-10-09, M4): it says the mesh's controller or the mesh's vault asks, which the bus
|
||||
// makes true — it denies these subjects to every principal but those two, and lets the vault alone call them in
|
||||
// its name — and a caller's own words are refused, so nobody puts a prompt of its choosing before the operator.
|
||||
func secretQuestion(caller string, args map[string]any) (SecretQuestion, error) {
|
||||
var q SecretQuestion
|
||||
for _, free := range []string{"prompt", "message"} {
|
||||
if _, there := args[free]; there {
|
||||
@@ -81,14 +93,41 @@ func secretQuestion(args map[string]any) (SecretQuestion, error) {
|
||||
}
|
||||
askedBy = v
|
||||
}
|
||||
q.Prompt = names["secret"] + " for " + names["module"]
|
||||
q.Message = fmt.Sprintf("The mesh's controller asks for %s, the own secret of %s on %s.", names["secret"],
|
||||
names["module"], names["node"])
|
||||
if askedBy != "" {
|
||||
q.Message += fmt.Sprintf(" It was asked for by %s.", askedBy)
|
||||
// What the secret is for, in the catalogue's own words (novox/hq issue 789): the controller's own line, read
|
||||
// from the secret's declared `prompt` and never a word the caller chose. One line, so it is shown under the
|
||||
// module and secret names as its own; its characters are never markup, whatever the catalogue holds.
|
||||
what := ""
|
||||
if raw, there := args["what"]; there {
|
||||
v, err := text(args, "what", true)
|
||||
if err != nil {
|
||||
return q, err
|
||||
}
|
||||
if strings.ContainsAny(v, "\n\r") {
|
||||
return q, fmt.Errorf("what %q holds more than one line", raw)
|
||||
}
|
||||
what = v
|
||||
}
|
||||
q.Message += " What you type is not shown, and is sealed before it leaves this machine. Type it only if you " +
|
||||
q.Prompt = names["secret"] + " for " + names["module"]
|
||||
asker := "controller"
|
||||
if theVault(caller) {
|
||||
asker = "vault"
|
||||
}
|
||||
q.Message = fmt.Sprintf("The mesh's %s asks for %s, the own secret of %s on %s.", asker, names["secret"],
|
||||
names["module"], names["node"])
|
||||
if what != "" {
|
||||
q.Message += "\nFor: " + escapeMarkup(what)
|
||||
}
|
||||
tail := ""
|
||||
if askedBy != "" {
|
||||
tail += fmt.Sprintf(" It was asked for by %s.", askedBy)
|
||||
}
|
||||
tail += " What you type is not shown, and is sealed before it leaves this machine. Type it only if you " +
|
||||
"expect this ask."
|
||||
if what != "" {
|
||||
q.Message += "\n" + strings.TrimSpace(tail)
|
||||
} else {
|
||||
q.Message += tail
|
||||
}
|
||||
key, err := text(args, "seal_to", true)
|
||||
if err != nil {
|
||||
return q, err
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestWhatIsTypedIsAnsweredSealedToTheAskersKeyAndNeverInTheClear(t *testing.
|
||||
bin := fakeBinaries(t, map[string]string{"rofi": `echo "$*" > "$LOG"; echo "` + typedValue + `"`})
|
||||
t.Setenv("LOG", filepath.Join(bin, "log"))
|
||||
key, pub, priv := aKey(t)
|
||||
q, err := secretQuestion(map[string]any{"module": "telegram", "secret": "telegram-token", "node": "anchor", "seal_to": key})
|
||||
q, err := secretQuestion("unproven", map[string]any{"module": "telegram", "secret": "telegram-token", "node": "anchor", "seal_to": key})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func TestWhatIsTypedIsAnsweredSealedToTheAskersKeyAndNeverInTheClear(t *testing.
|
||||
func TestADismissedEmptyOrLatePromptIsACancelAndAFailureNeverSaysTheValue(t *testing.T) {
|
||||
withSession(t)
|
||||
key, _, _ := aKey(t)
|
||||
q, _ := secretQuestion(map[string]any{"module": "telegram", "secret": "telegram-token", "node": "anchor", "seal_to": key, "timeout_seconds": 1})
|
||||
q, _ := secretQuestion("unproven", map[string]any{"module": "telegram", "secret": "telegram-token", "node": "anchor", "seal_to": key, "timeout_seconds": 1})
|
||||
for script, timedOut := range map[string]bool{`exit 1`: false, `echo ""`: false, `sleep 30`: true} {
|
||||
fakeBinaries(t, map[string]string{"rofi": script})
|
||||
got, err := Secret(q)
|
||||
@@ -92,7 +92,7 @@ func TestASecretIsAskedOnlyWithAKeyToSealItTo(t *testing.T) {
|
||||
named(map[string]any{"seal_to": key, "timeout_seconds": 60}),
|
||||
{"seal_to": key},
|
||||
} {
|
||||
if _, err := secretQuestion(args); err == nil {
|
||||
if _, err := secretQuestion("unproven", args); err == nil {
|
||||
t.Errorf("%v was taken", args)
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func TestASecretIsAskedOnlyWithAKeyToSealItTo(t *testing.T) {
|
||||
func TestThePromptIsWrittenHereFromNamesAndSaysTheControllerAsks(t *testing.T) {
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
|
||||
base := map[string]any{"module": "telegram", "secret": "telegram-token", "node": "anchor", "seal_to": key}
|
||||
q, err := secretQuestion(base)
|
||||
q, err := secretQuestion("unproven", base)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -112,18 +112,26 @@ func TestThePromptIsWrittenHereFromNamesAndSaysTheControllerAsks(t *testing.T) {
|
||||
strings.Contains(q.Message, "asked for by") {
|
||||
t.Errorf("prompt %q, message %q", q.Prompt, q.Message)
|
||||
}
|
||||
// Asked by the vault's own account, as the runtime proved it (issue 723): the prompt says the vault asks.
|
||||
if q, err := secretQuestion("novox.mesh-vault", base); err != nil ||
|
||||
!strings.HasPrefix(q.Message, "The mesh's vault asks for telegram-token") {
|
||||
t.Errorf("asked by the vault: %q %v", q.Message, err)
|
||||
}
|
||||
if q, _ := secretQuestion("novox.mesh-vault-not", base); !strings.HasPrefix(q.Message, "The mesh's controller") {
|
||||
t.Errorf("a caller that is not the vault's account is said to be the vault: %q", q.Message)
|
||||
}
|
||||
// And who asked the controller, when it says so (novox/hq ADR 0277): a caller's name, in a name's characters.
|
||||
withAsker := map[string]any{}
|
||||
for k, v := range base {
|
||||
withAsker[k] = v
|
||||
}
|
||||
withAsker["asked_by"] = "g14/claude-code"
|
||||
q, err = secretQuestion(withAsker)
|
||||
q, err = secretQuestion("unproven", withAsker)
|
||||
if err != nil || !strings.Contains(q.Message, "It was asked for by g14/claude-code.") {
|
||||
t.Errorf("asked_by: %q %v", q.Message, err)
|
||||
}
|
||||
withAsker["asked_by"] = "jochen at a shell on novox"
|
||||
if q, err = secretQuestion(withAsker); err != nil || !strings.Contains(q.Message, "It was asked for by jochen at a shell on novox.") {
|
||||
if q, err = secretQuestion("unproven", withAsker); err != nil || !strings.Contains(q.Message, "It was asked for by jochen at a shell on novox.") {
|
||||
t.Errorf("asked_by at a shell: %q %v", q.Message, err)
|
||||
}
|
||||
for key, value := range map[string]any{
|
||||
@@ -139,14 +147,72 @@ func TestThePromptIsWrittenHereFromNamesAndSaysTheControllerAsks(t *testing.T) {
|
||||
args[k] = v
|
||||
}
|
||||
args[key] = value
|
||||
if _, err := secretQuestion(args); err == nil {
|
||||
if _, err := secretQuestion("unproven", args); err == nil {
|
||||
t.Errorf("%s=%q was taken", key, value)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"", "a\nb", "a<b>c", "a,b", strings.Repeat("a", 81), "a b", "a:b", "a'b"} {
|
||||
withAsker["asked_by"] = bad
|
||||
if _, err := secretQuestion(withAsker); err == nil {
|
||||
if _, err := secretQuestion("unproven", withAsker); err == nil {
|
||||
t.Errorf("asked_by=%q was taken", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What the secret is for shows as its own line under the module and secret names, in the catalogue's own
|
||||
// words (novox/hq issue 789): read from the secret's declared `prompt`, never a word the caller chose, and
|
||||
// never shown without a declared one — an old module, or one that declares none, prompts as it does today.
|
||||
func TestWhatShowsAsItsOwnLineUnderTheNamesWhenDeclaredAndNothingWhenNot(t *testing.T) {
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
|
||||
base := map[string]any{"module": "mesh-mail", "secret": "mail-credential-gmail", "node": "anchor", "seal_to": key}
|
||||
q, err := secretQuestion("unproven", base)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(q.Message, "For:") {
|
||||
t.Errorf("a \"For:\" line with no what declared: %q", q.Message)
|
||||
}
|
||||
|
||||
withWhat := map[string]any{}
|
||||
for k, v := range base {
|
||||
withWhat[k] = v
|
||||
}
|
||||
withWhat["what"] = "the password of the mail account gmail"
|
||||
q, err = secretQuestion("unproven", withWhat)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(q.Message, "\nFor: the password of the mail account gmail") {
|
||||
t.Errorf("no \"For:\" line of its own: %q", q.Message)
|
||||
}
|
||||
if !strings.Contains(q.Message, "What you type is not shown") {
|
||||
t.Errorf("the rest of the message was lost: %q", q.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// What the secret is for is shown as text, never markup (novox/hq issue 789): the prompt's window reads its
|
||||
// message with markup on, so what the catalogue holds reaches it with the characters markup gives meaning to
|
||||
// turned to their entities first, and a line holding more than one is refused before anybody sees it.
|
||||
func TestWhatIsShownAsTextNeverMarkupAndIsOneLine(t *testing.T) {
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
|
||||
base := map[string]any{"module": "mesh-mail", "secret": "mail-credential-gmail", "node": "anchor", "seal_to": key,
|
||||
"what": "the password of <b>the</b> mail & account"}
|
||||
q, err := secretQuestion("unproven", base)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(q.Message, "For: the password of <b>the</b> mail & account") {
|
||||
t.Errorf("markup was not escaped: %q", q.Message)
|
||||
}
|
||||
|
||||
for _, bad := range []string{"a\nb", "a\rb"} {
|
||||
multi := map[string]any{}
|
||||
for k, v := range base {
|
||||
multi[k] = v
|
||||
}
|
||||
multi["what"] = bad
|
||||
if _, err := secretQuestion("unproven", multi); err == nil {
|
||||
t.Errorf("what=%q holding more than one line was taken", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ module rofi
|
||||
|
||||
go 1.22
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.7
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.31.0
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009 h1:I9rm8HhYRJ0+mzQQz+VAH0kgv2YJWkvctEc67Yd0PX8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
|
||||
+16
-10
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user