Files
mesh-host/internal/apply/vocabulary_test.go
T
jschoubben 8e12b3c9e4 Name the decisions these tests defend, and check the bundle at all
From auditing the decision records: of 28, only 12 were named by any
test, so "which decisions are defended" could not be answered without
reading everything. ADR 0017 says a test names the decision it defends —
that rule was itself unenforced.

Most of the gap was citation, not coverage. Drift detection was tested
in several places without naming ADR 0011; the archive refusal without
naming 0012; forged declarations without naming 0002. Named now, so the
question is answerable by grep.

The bundle was the real gap: nothing tested substrate-first-node.lock at
all. It is what a machine becomes when there is no mesh to ask — the one
declaration applied with nothing to verify it against — and it was
edited by hand and read by nothing but a running host.

Two tests now assert what it carries: exactly postgres, lavinmq and the
control plane. That defends ADR 0028, which removed the object store
from the substrate after it had been a member for months on the strength
of "it cannot grant itself a bucket" — true, and the answer to only half
the test. Nothing counted what the bundle held.

Fault-injected, and the first attempt did not bite: the injection landed
on a comment line, which stripComments discards. Injecting into the
image field fails as it should.
2026-08-31 17:33:34 +02:00

251 lines
8.3 KiB
Go

package apply
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/novox/mesh-host/internal/declaration"
"github.com/novox/mesh-host/internal/store"
)
// The shapes added so that most of what a person installs is expressible.
//
// A shell, a chat client, a desktop are a package plus configuration in somebody's home, and a
// mesh with no user can manage /etc and nothing anybody looks at.
func declare(t *testing.T, resources string) *declaration.Declaration {
t.Helper()
d, err := declaration.Parse([]byte(`{"declaration":1,"resources":[` + resources + `]}`))
if err != nil {
t.Fatal(err)
}
return d
}
func TestAFileMayBeBytesRatherThanText(t *testing.T) {
// A wallpaper, a font, an icon. Stored as its own encoding it would be a wallpaper nothing
// can open.
dir := t.TempDir()
original := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0xff}
d := declare(t, `{"id":"w","type":"file","path":"`+dir+`/wall.png","bytes":"`+
base64.StdEncoding.EncodeToString(original)+`"}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, noServices, nil, nil); err != nil {
t.Fatal(err)
}
on, err := os.ReadFile(dir + "/wall.png")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(on, original) {
t.Fatalf("the bytes did not survive: %x", on)
}
}
func TestAFileSaysWhatIsInItExactlyOnce(t *testing.T) {
// Three ways of saying it and no precedence between them, so "what is in this file" is
// answerable by looking rather than by knowing which field wins.
_, err := declaration.Parse([]byte(`{"declaration":1,"resources":[
{"id":"f","type":"file","path":"/etc/x","content":"a","bytes":"YQ=="}]}`))
if err == nil {
t.Fatal("a file that was both text and bytes was accepted")
}
if !strings.Contains(err.Error(), "exactly once") {
t.Fatalf("unhelpful refusal: %v", err)
}
}
func TestBytesThatAreNotBase64AreRefused(t *testing.T) {
dir := t.TempDir()
d := declare(t, `{"id":"w","type":"file","path":"`+dir+`/x","bytes":"not base64!!"}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, noServices, nil, nil)
if err == nil {
t.Fatal("a file carrying nonsense was written")
}
if _, statErr := os.Stat(dir + "/x"); statErr == nil {
t.Fatal("something was written before the failure")
}
}
// A gzipped tar, and its digest, built here so the test does not depend on a fixture nobody can
// regenerate.
func anArchive(t *testing.T, files map[string]string) ([]byte, string) {
t.Helper()
var raw bytes.Buffer
zipped := gzip.NewWriter(&raw)
writer := tar.NewWriter(zipped)
for name, body := range files {
if err := writer.WriteHeader(&tar.Header{
Name: name, Mode: 0o644, Size: int64(len(body)), Typeflag: tar.TypeReg,
}); err != nil {
t.Fatal(err)
}
if _, err := writer.Write([]byte(body)); err != nil {
t.Fatal(err)
}
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
if err := zipped.Close(); err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(raw.Bytes())
return raw.Bytes(), "sha256:" + hex.EncodeToString(sum[:])
}
func serving(t *testing.T, body []byte) string {
t.Helper()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write(body)
}))
t.Cleanup(server.Close)
return server.URL + "/theme.tar.gz"
}
func TestAnArchiveIsUnpacked(t *testing.T) {
body, digest := anArchive(t, map[string]string{
"config/theme.conf": "dark", "config/icons/one.svg": "<svg/>",
})
dir := t.TempDir()
d := declare(t, `{"id":"theme","type":"archive","source":"`+serving(t, body)+
`","digest":"`+digest+`","path":"`+dir+`/theme"}`)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if !report.Changed() {
t.Fatal("nothing changed")
}
on, err := os.ReadFile(dir + "/theme/config/theme.conf")
if err != nil {
t.Fatal(err)
}
if string(on) != "dark" {
t.Fatalf("got %q", on)
}
}
func TestAnArchiveThatIsNotWhatWasDeclaredIsRefusedBeforeAnythingIsWritten(t *testing.T) {
// The only thing making bytes from a network the mesh does not control safe to unpack is
// that they hash to what was declared.
body, _ := anArchive(t, map[string]string{"a": "b"})
dir := t.TempDir()
d := declare(t, `{"id":"theme","type":"archive","source":"`+serving(t, body)+
`","digest":"sha256:`+strings.Repeat("ab", 32)+`","path":"`+dir+`/theme"}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, noServices, nil, nil)
if err == nil {
t.Fatal("an archive that was not what was declared was unpacked")
}
if entries, _ := os.ReadDir(dir); len(entries) != 0 {
t.Fatal("something was written before the digest was checked")
}
}
func TestAnArchiveCannotWriteOutsideWhereItWasUnpacked(t *testing.T) {
// The oldest bug in unpacking. Checked against the resolved root rather than by looking for
// "..", because there is more than one way to name a path that escapes.
body, digest := anArchive(t, map[string]string{"../../escaped": "no"})
dir := t.TempDir()
d := declare(t, `{"id":"theme","type":"archive","source":"`+serving(t, body)+
`","digest":"`+digest+`","path":"`+dir+`/theme"}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, noServices, nil, nil)
if err != nil && !strings.Contains(err.Error(), "outside") {
t.Fatalf("refused for the wrong reason: %v", err)
}
if _, statErr := os.Stat(dir + "/escaped"); statErr == nil {
t.Fatal("a file landed outside the directory it was unpacked into")
}
if err == nil {
t.Fatal("an escaping entry was accepted")
}
}
func TestAnUnpackedArchiveIsNotFetchedAgainForNothing(t *testing.T) {
// The digest is the whole identity of an archive, so a matching record means the tree came
// from these exact bytes. Applying twice must not report work.
body, digest := anArchive(t, map[string]string{"a": "b"})
dir := t.TempDir()
d := declare(t, `{"id":"theme","type":"archive","source":"`+serving(t, body)+
`","digest":"`+digest+`","path":"`+dir+`/theme"}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
again, _, err := Apply(context.Background(), archHost(t), d, state,
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if again.Changed() {
said, _ := json.Marshal(again)
t.Fatalf("the second apply did work: %s", said)
}
}
// Defends novox/hq ADR 0012: the mesh creates no symlinks — a derived file is a copy.
//
// The archive is the one path where a symlink could arrive without anybody declaring it, which is
// why the refusal lives here. ADR 0012 was earned by production data loss through a symlink
// resolved inside a container volume path.
func TestAnArchiveWithSomethingThatIsNotAFileIsRefused(t *testing.T) {
// A theme needing a symlink would otherwise arrive silently incomplete, and a device node in
// an archive is not something to unpack quietly onto a machine.
var raw bytes.Buffer
zipped := gzip.NewWriter(&raw)
writer := tar.NewWriter(zipped)
if err := writer.WriteHeader(&tar.Header{
Name: "link", Typeflag: tar.TypeSymlink, Linkname: "/etc/passwd", Mode: 0o777,
}); err != nil {
t.Fatal(err)
}
writer.Close()
zipped.Close()
sum := sha256.Sum256(raw.Bytes())
digest := "sha256:" + hex.EncodeToString(sum[:])
dir := t.TempDir()
d := declare(t, `{"id":"theme","type":"archive","source":"`+serving(t, raw.Bytes())+
`","digest":"`+digest+`","path":"`+dir+`/theme"}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, noServices, nil, nil)
if err == nil {
t.Fatal("a symlink was unpacked")
}
if !strings.Contains(err.Error(), "files and directories") {
t.Fatalf("refused for the wrong reason: %v", err)
}
}
func TestAnArchiveMustBePinned(t *testing.T) {
_, err := declaration.Parse([]byte(`{"declaration":1,"resources":[
{"id":"t","type":"archive","source":"https://example.invalid/a.tgz","path":"/opt/t"}]}`))
if err == nil {
t.Fatal("an unpinned archive was accepted")
}
if !strings.Contains(err.Error(), "digest") {
t.Fatalf("unhelpful refusal: %v", err)
}
}