Files
mesh-host/internal/meshcli/meshcli_test.go
T
jochen 564948c2f2
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
mesh-cli: follow a line to its answer, bound each account, refuse other namespaces (review of hq ADR 0272)
- A request that timed out was said as "the controller was not reached,
  nothing ran", though the line may have run. The engine now follows a line
  the controller answers as running, by its call, until it ends; when no
  answer comes in time it says the line may have run and names the call.
  "Nothing ran" is said only of a request no controller took.
- One account could hold the socket: accepting waited for a free slot, and
  a connection could stall before its deadline was set. The deadline is set
  at accept, before any lookup; each user id has two lines at once and the
  machine eight, and one over is answered busy at once.
- A container running with the operator's user id shares the number, not
  the machine: a peer whose mount or user namespace is not the engine's is
  refused, and nothing is asked.

SDK pinned at the go/cli branch's newest commit until go/v0.1.9 is tagged.
2026-10-09 12:48:20 +02:00

219 lines
7.1 KiB
Go

package meshcli
import (
"context"
"encoding/json"
"errors"
"net"
"os"
"os/user"
"path/filepath"
"sort"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/cli"
)
// serving starts Serve on a socket of the test's own, and stops it when the test ends.
func serving(t *testing.T, forward Forward) string {
t.Helper()
socket := filepath.Join(t.TempDir(), "run", "cli.sock")
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() { done <- Serve(ctx, socket, forward, func(string) {}) }()
t.Cleanup(func() {
cancel()
if err := <-done; err != nil {
t.Errorf("Serve ended with %v", err)
}
})
for i := 0; i < 200; i++ {
if _, err := os.Stat(socket); err == nil {
return socket
}
time.Sleep(5 * time.Millisecond)
}
t.Fatal("the socket never appeared")
return ""
}
// The account sent is the one the kernel gives for the connecting process, and the answer comes back as it was.
func TestTheAccountAskingIsTheKernelsAndTheAnswerComesBack(t *testing.T) {
var got Asked
socket := serving(t, func(_ context.Context, asked Asked) cli.Answer {
got = asked
return cli.Answer{Stdout: []byte("done\n"), Exit: 0, Terminal: true, Why: "the terminal"}
})
a, err := cli.Ask(context.Background(), socket, []string{"settings", "show", "zsh"})
if err != nil {
t.Fatal(err)
}
if string(a.Stdout) != "done\n" || !a.Terminal || a.Refused != "" {
t.Fatalf("the answer came back as %+v", a)
}
me, err := user.Current()
if err != nil {
t.Skipf("this machine cannot name the test's account: %v", err)
}
if int(got.UID) != os.Getuid() || got.Account != me.Username {
t.Fatalf("asked as %d %q, and the kernel says %d %q", got.UID, got.Account, os.Getuid(), me.Username)
}
if strings.Join(got.Line, " ") != "settings show zsh" {
t.Fatalf("the line was sent as %q", got.Line)
}
if info, err := os.Stat(socket); err != nil || info.Mode().Perm() != 0o666 {
t.Fatalf("the socket is not open to every account: %v %v", info.Mode(), err)
}
}
// Credentials the kernel did not give are no account: refused, and nothing is asked.
func TestAConnectionWhoseAccountCannotBeReadIsNeverAskedAsAnyone(t *testing.T) {
before := peerOf
peerOf = func(*net.UnixConn) (uint32, int32, error) { return 0, 0, errors.New("no credentials") }
t.Cleanup(func() { peerOf = before })
asked := false
socket := serving(t, func(context.Context, Asked) cli.Answer {
asked = true
return cli.Answer{}
})
a, err := cli.Ask(context.Background(), socket, []string{"status"})
if err != nil {
t.Fatal(err)
}
if asked || a.Refused == "" || a.Exit == 0 {
t.Fatalf("a connection with no account was asked (%v) or not refused: %+v", asked, a)
}
}
// A request over the bound is refused and never asked.
func TestARequestOverTheBoundIsRefused(t *testing.T) {
asked := false
socket := serving(t, func(context.Context, Asked) cli.Answer {
asked = true
return cli.Answer{}
})
a, err := cli.Ask(context.Background(), socket, []string{strings.Repeat("x", cli.MaxRequest)})
if err != nil {
t.Fatal(err)
}
if asked || a.Refused == "" {
t.Fatalf("a request over the bound was asked (%v): %+v", asked, a)
}
}
// A file at the socket's path that is not a socket is never removed.
func TestAFileThatIsNotASocketIsKept(t *testing.T) {
dir := t.TempDir()
socket := filepath.Join(dir, "cli.sock")
if err := os.WriteFile(socket, []byte("mine"), 0o644); err != nil {
t.Fatal(err)
}
err := Serve(context.Background(), socket, nil, nil)
if err == nil || !strings.Contains(err.Error(), "not a socket") {
t.Fatalf("a file at the socket's path was not refused: %v", err)
}
if body, _ := os.ReadFile(socket); string(body) != "mine" {
t.Fatal("the file at the socket's path was touched")
}
}
// The request the controller reads holds these field names; the controller's side holds the same list
// (mesh-controller, the mesh-cli request's test).
func TestTheRequestToTheControllerKeepsItsFieldNames(t *testing.T) {
body, err := json.Marshal(Asked{Line: []string{"status"}, Account: "a", UID: 1})
if err != nil {
t.Fatal(err)
}
if got := keysOf(t, body); got != "account line uid" {
t.Fatalf("the request's fields are %q", got)
}
body, _ = json.Marshal(Asked{Follow: "call-1", Account: "a", UID: 1})
if got := keysOf(t, body); got != "account follow uid" {
t.Fatalf("a follow's fields are %q", got)
}
// What the controller answers as a line's result, and as a running line's: the same names its side holds.
body, err = json.Marshal(cli.Answer{Stdout: []byte("o"), Stderr: []byte("e"), Exit: 1, Terminal: true, Why: "w",
Refused: "r", Cut: true})
if err != nil {
t.Fatal(err)
}
if got := keysOf(t, body); got != "cut exit refused stderr stdout terminal why" {
t.Fatalf("the answer's fields are %q", got)
}
body, _ = json.Marshal(runningResult{Running: true, Call: "call-1"})
if got := keysOf(t, body); got != "call running" {
t.Fatalf("a running answer's fields are %q", got)
}
}
func keysOf(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, " ")
}
// **One account cannot hold the socket** (review of ADR 0272): its lines beyond its own bound are answered busy at
// once, nothing is asked, and the engine goes on accepting.
func TestOneAccountsLinesBeyondItsBoundAreAnsweredBusy(t *testing.T) {
was := perAccount
perAccount = 1
t.Cleanup(func() { perAccount = was })
release := make(chan struct{})
started := make(chan struct{}, 2)
socket := serving(t, func(context.Context, Asked) cli.Answer {
started <- struct{}{}
<-release
return cli.Answer{}
})
// Released before the engine is stopped: cleanups run last first, and the engine waits for its lines.
t.Cleanup(func() { close(release) })
go func() { _, _ = cli.Ask(context.Background(), socket, []string{"push"}) }()
<-started
done := make(chan cli.Answer, 1)
go func() {
a, _ := cli.Ask(context.Background(), socket, []string{"status"})
done <- a
}()
select {
case a := <-done:
if !strings.Contains(a.Refused, "busy") {
t.Fatalf("a second line from the same account reads %+v", a)
}
case <-time.After(5 * time.Second):
t.Fatal("a second line from the same account waited instead of being answered busy")
}
if len(started) != 0 {
t.Fatal("the busy line was asked")
}
}
// **A process in another namespace is not this machine's account** (review of ADR 0272): a container running with
// the operator's user id shares the id and not the machine; refused, and nothing is asked.
func TestAPeerInAnotherNamespaceIsRefused(t *testing.T) {
before := sameNamespaces
sameNamespaces = func(pid int32) error { return errors.New("its mount namespace is not the engine's") }
t.Cleanup(func() { sameNamespaces = before })
asked := false
socket := serving(t, func(context.Context, Asked) cli.Answer {
asked = true
return cli.Answer{}
})
a, err := cli.Ask(context.Background(), socket, []string{"status"})
if err != nil {
t.Fatal(err)
}
if asked || !strings.Contains(a.Refused, "namespace") {
t.Fatalf("a peer in another namespace was asked (%v): %+v", asked, a)
}
}