mesh/merge-gate head dae79c76434c merged into trunk cd13f7726ef0 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head dae79c76434c merged into trunk cd13f7726ef0 — pass: its merge-check.sh passed
mesh/delivery delivered
The merge check's build container has no pacman, so the acceptance test now stubs it and names the host it was built for, as other command tests do; nothing in the node-engine itself changes.
114 lines
4.2 KiB
Go
114 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
osuser "os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/apply"
|
|
)
|
|
|
|
// TestAMoveOutDirectoryKeepsOnlyWhatItKnows drives the node-engine whole through its command (issue 1024, novox/hq
|
|
// ADR 0422): a directory under an account's home that says move-out keeps the file the mesh declared and moves a
|
|
// file nobody declared into the kept folder, named in a list, and the run says it moved it. It needs no bus: the
|
|
// apply is the one a delivery and the five-minute reconcile both run.
|
|
func TestAMoveOutDirectoryKeepsOnlyWhatItKnows(t *testing.T) {
|
|
me, err := osuser.Current()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The user database lists an account as a person's only at uid 1000 or more; root, as the forge's merge check
|
|
// runs, is listed under a number of its own (it can give files to it), by the same name.
|
|
listedUID := me.Uid
|
|
if uid, _ := strconv.Atoi(me.Uid); uid < 1000 || uid == 65534 {
|
|
listedUID = "4242"
|
|
}
|
|
root := t.TempDir()
|
|
home := filepath.Join(root, "home", "agent")
|
|
dir := filepath.Join(home, ".ssh")
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
passwd := filepath.Join(root, "passwd")
|
|
line := fmt.Sprintf("%s:x:%s:%s::%s:/bin/sh\n", me.Username, listedUID, me.Gid, home)
|
|
if err := os.WriteFile(passwd, []byte(line), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(apply.UseAccountsFileForTest(passwd))
|
|
// Independent of the machine: an arch host with a pacman on PATH that answers and holds nothing, as
|
|
// TestReplay233 puts one there (it answers with an error); nothing here declares a package.
|
|
bin := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(bin, "pacman"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
wasBuilt := builtFor
|
|
builtFor = "arch"
|
|
t.Cleanup(func() { builtFor = wasBuilt })
|
|
|
|
stray := filepath.Join(dir, "id_stray")
|
|
if err := os.WriteFile(stray, []byte("somebody else's key"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old := time.Now().Add(-time.Hour)
|
|
if err := os.Chtimes(stray, old, old); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pipe := filepath.Join(dir, "agent.sock")
|
|
if err := syscall.Mkfifo(pipe, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chtimes(pipe, old, old); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts := options{state: filepath.Join(root, "state.json")}
|
|
raw := []byte(`{"declaration":1,"resources":[` +
|
|
`{"id":"ssh.dir","type":"directory","path":"` + dir + `","owner":"` + me.Username + `","unknown":"move-out"},` +
|
|
`{"id":"ssh.config","type":"file","path":"` + dir + `/config","content":"Host *\n","owner":"` + me.Username + `"}]}`)
|
|
|
|
var said []string
|
|
report := applyAndKeep(context.Background(), opts, raw, nil, nil, func(s string) { said = append(said, s) })
|
|
|
|
if report.Refused != "" || len(report.Failed) > 0 {
|
|
t.Fatalf("refused or failed: %q %v", report.Refused, report.Failed)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "config")); err != nil {
|
|
t.Errorf("the declared file is not there: %v", err)
|
|
}
|
|
if _, err := os.Lstat(stray); err == nil {
|
|
t.Error("the file nobody declared stayed in the directory")
|
|
}
|
|
lists, _ := filepath.Glob(filepath.Join(home, ".local", "state", "mesh-moved", "ssh", "*.json"))
|
|
if len(lists) != 1 {
|
|
t.Fatalf("one list expected: %v", lists)
|
|
}
|
|
rawList, err := os.ReadFile(lists[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var entries []struct{ From, To string }
|
|
if err := json.Unmarshal(rawList, &entries); err != nil || len(entries) != 1 {
|
|
t.Fatalf("the list: %s %v", rawList, err)
|
|
}
|
|
if got, err := os.ReadFile(entries[0].To); err != nil || string(got) != "somebody else's key" {
|
|
t.Errorf("the moved file is not whole in the kept folder: %q %v", got, err)
|
|
}
|
|
if len(report.MovedOut) != 1 || report.MovedOut[0].From != stray || report.MovedOut[0].To != entries[0].To {
|
|
t.Errorf("the report does not carry the move: %+v", report.MovedOut)
|
|
}
|
|
if len(report.Left) != 1 || report.Left[0].Path != pipe {
|
|
t.Errorf("the report does not list the pipe left in place: %+v", report.Left)
|
|
}
|
|
if !strings.Contains(strings.Join(said, "\n"), "moved "+stray) {
|
|
t.Errorf("the run does not say it moved the file: %v", said)
|
|
}
|
|
}
|