215 lines
9.1 KiB
Go
215 lines
9.1 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// The node-engine's request and the answer it hands mesh-cli hold these field names; the engine's side holds the
|
|
// same list (mesh-host internal/meshcli, TestTheRequestToTheControllerKeepsItsFieldNames).
|
|
func TestTheMeshCLIRequestAndAnswerKeepTheirFieldNames(t *testing.T) {
|
|
body, _ := json.Marshal(CLIAsked{Line: []string{"status"}, Account: "a", UID: 1, Session: "session-3.scope"})
|
|
if got := keysIn(t, body); got != "account line session uid" {
|
|
t.Fatalf("the request's fields are %q", got)
|
|
}
|
|
body, _ = json.Marshal(CLIAsked{Line: []string{"status"}, Account: "a", UID: 1})
|
|
if got := keysIn(t, body); got != "account line uid" {
|
|
t.Fatalf("the request's fields are %q", got)
|
|
}
|
|
body, _ = json.Marshal(CLIAnswer{Stdout: []byte("o"), Stderr: []byte("e"), Exit: 1, Terminal: true, Why: "w",
|
|
Refused: "r", Cut: true})
|
|
if got := keysIn(t, body); got != "cut exit refused stderr stdout terminal why" {
|
|
t.Fatalf("the answer's fields are %q", got)
|
|
}
|
|
body, _ = json.Marshal(CLIAsked{Line: []string{"secret"}, Account: "a", UID: 1, Stdin: []byte("x")})
|
|
if got := keysIn(t, body); got != "account line stdin uid" {
|
|
t.Fatalf("a request with standard input has the fields %q", got)
|
|
}
|
|
body, _ = json.Marshal(CLIAsked{Follow: "call-1", Account: "a", UID: 1})
|
|
if got := keysIn(t, body); got != "account follow uid" {
|
|
t.Fatalf("a follow's fields are %q", got)
|
|
}
|
|
// What a line still running answers, in the envelope every call's answer is in: `result`, then these.
|
|
var env struct {
|
|
Result json.RawMessage `json:"result"`
|
|
}
|
|
_ = json.Unmarshal(running(&Call{ID: "call-1", Seat: CLISeat, Verb: "a"}, AnswerWithin, false, ""), &env)
|
|
if got := keysIn(t, env.Result); got != "call output running started" {
|
|
t.Fatalf("a running answer's fields are %q", got)
|
|
}
|
|
}
|
|
|
|
func keysIn(t *testing.T, body []byte) string {
|
|
t.Helper()
|
|
var m map[string]any
|
|
if err := json.Unmarshal(body, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var keys []string
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return strings.Join(keys, " ")
|
|
}
|
|
|
|
// A node's mesh-cli subject names the node, and no other subject does.
|
|
func TestTheMeshCLISubjectNamesItsNode(t *testing.T) {
|
|
if node, ok := CLINode(CLISubject("laptop")); !ok || node != "laptop" {
|
|
t.Fatalf("CLINode(%q) = %q %v", CLISubject("laptop"), node, ok)
|
|
}
|
|
for _, s := range []string{"mesh.control.laptop.report", "mesh.control..cli", "mesh.control.a.b.cli", "mesh.node.a.cli"} {
|
|
if _, ok := CLINode(s); ok {
|
|
t.Fatalf("%s was read as a mesh-cli subject", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// An answer larger than one bus message is cut to fit, and the cut is said.
|
|
func TestALargeMeshCLIAnswerIsCutAndSaysSo(t *testing.T) {
|
|
a := CLIAnswer{Stdout: []byte(strings.Repeat("o", 200000)), Stderr: []byte(strings.Repeat("e", 1000)), Why: "the terminal"}
|
|
body := FitCLIAnswer(a, 64<<10)
|
|
if len(body) > 64<<10 {
|
|
t.Fatalf("the answer is %d bytes, over the bound", len(body))
|
|
}
|
|
var got CLIAnswer
|
|
if err := json.Unmarshal(body, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !got.Cut || got.Why != "the terminal" || len(got.Stdout) == 0 || string(got.Stderr) != strings.Repeat("e", 1000) {
|
|
t.Fatalf("the cut answer is %+v", got)
|
|
}
|
|
if small := FitCLIAnswer(CLIAnswer{Stdout: []byte("ok")}, 64<<10); strings.Contains(string(small), `"cut"`) {
|
|
t.Fatal("an answer that fits was said to be cut")
|
|
}
|
|
}
|
|
|
|
// cliBus serves mesh-cli on a bus of the test's own with a call log of its own, and returns a connection to ask on.
|
|
|
|
// askCLI asks one request on a node's subject and reads the envelope.
|
|
func askCLI(t *testing.T, conn *nats.Conn, node string, asked CLIAsked) (CLIAnswer, map[string]any, string) {
|
|
t.Helper()
|
|
body, _ := json.Marshal(asked)
|
|
msg, err := conn.Request(CLISubject(node), body, 5*time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var env struct {
|
|
Result json.RawMessage `json:"result"`
|
|
Error string `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(msg.Data, &env); err != nil {
|
|
t.Fatalf("not an envelope: %s", msg.Data)
|
|
}
|
|
var a CLIAnswer
|
|
var raw map[string]any
|
|
_ = json.Unmarshal(env.Result, &a)
|
|
_ = json.Unmarshal(env.Result, &raw)
|
|
return a, raw, env.Error
|
|
}
|
|
|
|
// **A line that outlasts the bus's window for an answer is followed to its answer, never lost** (review of ADR
|
|
// 0272): the first answer says it is running and names its call, and following the call by the same account on
|
|
// the same node gives what the line printed once it ends. Another account, or another node, is told no such call.
|
|
|
|
// Lines running at once are bounded: one over the bound is answered busy at once, and nothing ran.
|
|
|
|
// A mesh-cli line's record keeps its first word, never the rest of its line, and its answer is never kept on the
|
|
// bus, where `calls` answers anyone who may call the seat.
|
|
func TestAMeshCLIRecordKeepsNeitherItsLineNorItsAnswerOnTheBus(t *testing.T) {
|
|
l, a := NewCallLog(), newAnswers(t)
|
|
writes := make(chan Call, 4)
|
|
l.writes = writes
|
|
asked, _ := json.Marshal(CLIAsked{Line: []string{"settings", "set", "x", `{"password":"s3cret"}`}, Account: "op"})
|
|
l.serveCall(CLISeat, "laptop", asked, "_INBOX.node.laptop.abcdefghijklmnopqrstuv",
|
|
func(context.Context, json.RawMessage) (any, error) {
|
|
return CLIAnswer{Stdout: []byte("s3cret-join")}, nil
|
|
},
|
|
a.respond, nil)
|
|
_ = a.only()
|
|
close(writes)
|
|
for c := range writes {
|
|
if carries(c.Args, "s3cret") || carries(c.Answer, "s3cret-join") {
|
|
t.Fatalf("the bus was sent %s / %s", c.Args, c.Answer)
|
|
}
|
|
if !strings.Contains(string(c.Args), "settings") || !strings.Contains(string(c.Args), `"op"`) {
|
|
t.Fatalf("the record does not say what was asked and by whom: %s", c.Args)
|
|
}
|
|
}
|
|
}
|
|
|
|
// `calls` answers anyone who may call the seat, agents among them: a mesh-cli line's answer is never shown there,
|
|
// even from the memory of the controller that ran it; every other call's is (review of ADR 0272).
|
|
func TestCallsNeverShowsAMeshCLILinesAnswer(t *testing.T) {
|
|
l, a := NewCallLog(), newAnswers(t)
|
|
asked, _ := json.Marshal(CLIAsked{Line: []string{"token", "issue", "x"}, Account: "operator"})
|
|
l.serveCall(CLISeat, "control", asked, "_INBOX.node.control.abcdefghijklmnopqrstuv",
|
|
func(context.Context, json.RawMessage) (any, error) {
|
|
return CLIAnswer{Stdout: []byte("s3cret-join")}, nil
|
|
},
|
|
a.respond, nil)
|
|
_ = a.only()
|
|
recent, _ := l.Recent()
|
|
shown, found, err := l.Shown(recent[0].ID)
|
|
if err != nil || !found {
|
|
t.Fatalf("the line is not shown at all: %v %v", found, err)
|
|
}
|
|
if carries(shown.Answer, "s3cret-join") {
|
|
t.Fatalf("calls shows a mesh-cli line's answer: %s", shown.Answer)
|
|
}
|
|
b := newAnswers(t)
|
|
l.serveCall("mesh-controller", "status", nil, "_INBOX.x.abcdefghijklmnopqrstuv",
|
|
func(context.Context, json.RawMessage) (any, error) { return "all well", nil }, b.respond, nil)
|
|
_ = b.only()
|
|
recent, _ = l.Recent()
|
|
if shown, _, _ := l.Shown(recent[0].ID); !strings.Contains(string(shown.Answer), "all well") {
|
|
t.Fatalf("another call's answer is withheld: %s", shown.Answer)
|
|
}
|
|
}
|
|
|
|
// carries says whether a record holds a secret as text or as the base64 JSON writes bytes in: a line's output is
|
|
// bytes, so a search for its text alone finds nothing whatever the record keeps (review of ADR 0272).
|
|
func carries(body []byte, secret string) bool {
|
|
return strings.Contains(string(body), secret) ||
|
|
strings.Contains(string(body), base64.StdEncoding.EncodeToString([]byte(secret)))
|
|
}
|
|
|
|
// The two tests above hold what they claim: each fails when the protection it names is taken away.
|
|
func TestTheWithholdingTestsHoldSomething(t *testing.T) {
|
|
// The answer as a mesh-cli line's record would carry it, were it kept: the search finds it.
|
|
body, _ := json.Marshal(map[string]any{"result": CLIAnswer{Stdout: []byte("s3cret-join")}})
|
|
if !carries(body, "s3cret-join") {
|
|
t.Fatalf("a record carrying the answer is not found carrying it: %s", body)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0259 §10: mesh-cli's standard input — a secret given at the terminal — is never in the calls record,
|
|
// in any form, whichever way the request reaches it.
|
|
func TestAMeshCLIRecordNeverKeepsItsStandardInput(t *testing.T) {
|
|
secret := "123456789:AAEhBOweik6ad9r_QxGivenAtTheTerminal"
|
|
raw, _ := json.Marshal(CLIAsked{Line: []string{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "-"},
|
|
Account: "operator", UID: 1000, Stdin: []byte(secret)})
|
|
for name, got := range map[string]json.RawMessage{"as answerCLI records it": kept(cliRecorded(raw)),
|
|
"as the record alone would keep it": kept(raw)} {
|
|
if carries(got, secret) || strings.Contains(string(got), base64.StdEncoding.EncodeToString([]byte(secret))[:20]) {
|
|
t.Fatalf("%s, the record keeps %s", name, got)
|
|
}
|
|
if !strings.Contains(string(got), "secret") || !strings.Contains(string(got), "given, not kept") &&
|
|
!strings.Contains(string(got), "stdin-given") {
|
|
t.Fatalf("%s, the record does not say a line was asked with standard input: %s", name, got)
|
|
}
|
|
}
|
|
// The record still reads as the request, so the asker can follow its call.
|
|
var asked CLIAsked
|
|
if err := json.Unmarshal(kept(cliRecorded(raw)), &asked); err != nil || asked.Account != "operator" || len(asked.Stdin) > 0 {
|
|
t.Fatalf("the recorded request reads as %+v (%v)", asked, err)
|
|
}
|
|
}
|