Files
mesh-host/internal/declaration/declaration_test.go
T
jschoubben 8c248e3d7f A secret can reach a container's environment, and sit inside a config file
Two gaps found by writing the first real module's manifest rather than
by reasoning about one. Both are fields on existing shapes, so the
vocabulary is still nine.

**env-file on a container.** A declaration reaches a node over the
broker and `env` is plain text in it, so a password there is a password
the broker sees — the transitive trust refused everywhere else. A sealed
file arrives unreadable, the host writes it, the runtime reads it. It is
also simply how third-party software takes credentials: nothing shipping
in a container will read a path the mesh invented, and every one of them
reads its environment.

**secrets in a file's content.** A program wanting its token inside a
JSON document cannot be handed a file that is entirely a token, and the
mesh cannot compose the document because it discarded the value. So the
module supplies the document with `${secret:name}` in it, the mesh
delivers the value sealed, and the host is the only thing that ever
holds both.

Substitution is textual and the host learns no formats. Deliberate: a
mechanism that understood JSON would be asked to understand YAML next,
and then INI, which is how the arrangement this replaces became
something nobody could hold in their head. The module knows its own
format because it wrote the rest of the file. The sharp edge is stated
rather than left to be discovered — a value containing a quote is not
escaped for whatever surrounds it.

Refused in both directions, because both are somebody being wrong about
where a credential is: a placeholder with nothing to fill it would write
`${secret:x}` into a config file, and a secret the content never uses
means somebody believes a credential is in a file where it is not.

A file that carries one is 0600 unless the module said otherwise.
2026-08-31 22:25:57 +02:00

385 lines
15 KiB
Go

package declaration
import (
"errors"
"strings"
"testing"
)
// Each test names the decision it defends (novox/hq ADR 0017). The decision here is ADR 0005,
// and the property it turns on is that unknown is REFUSED, never skipped.
func valid() string {
return `{"declaration":1,"resources":[
{"id":"etc","type":"directory","path":"/etc/mesh","mode":"0755"},
{"id":"conf","type":"file","path":"/etc/mesh/host.conf","content":"a\n","mode":"0640"},
{"id":"svc","type":"service","unit":"mesh-host.service","state":"running"}
]}`
}
func refusalFor(t *testing.T, raw string) *RefusalError {
t.Helper()
_, err := Parse([]byte(raw))
if err == nil {
t.Fatal("expected a refusal")
}
var refusal *RefusalError
if !errors.As(err, &refusal) {
t.Fatalf("expected a RefusalError, got %T: %v", err, err)
}
return refusal
}
func TestAValidDeclarationParsesInOrder(t *testing.T) {
d, err := Parse([]byte(valid()))
if err != nil {
t.Fatalf("unexpected refusal: %v", err)
}
// Order is stated, not derived. The host must not sort.
got := []string{d.Resources[0].Identity(), d.Resources[1].Identity(), d.Resources[2].Identity()}
want := []string{"etc", "conf", "svc"}
for i := range want {
if got[i] != want[i] {
t.Fatalf("resources reordered: %v, want %v", got, want)
}
}
}
func TestAnUnknownTypeRefusesTheWholeDeclaration(t *testing.T) {
// The property everything else rests on. A host that skipped what it did not understand
// would apply most of a declaration and report success — a node that looks configured and
// is not, which is 04-ISSUES/003 with the declaration on the other side of the wire.
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"ok","type":"directory","path":"/etc/mesh"},
{"id":"what","type":"blockchain","path":"/etc/mesh"}
]}`)
joined := strings.Join(refusal.Problems, "\n")
if !strings.Contains(joined, "blockchain") {
t.Errorf("the unknown type was not named: %v", refusal.Problems)
}
// And it must say what IS understood, or the reader goes to the source to find out.
if !strings.Contains(joined, "directory") || !strings.Contains(joined, "service") {
t.Errorf("the refusal does not say what this host understands: %v", refusal.Problems)
}
}
func TestAnUnknownFieldIsRefused(t *testing.T) {
// A field the host does not know is a thing the control plane believes it asked for.
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"conf","type":"file","path":"/etc/x","content":"a","immutable":true}
]}`)
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "immutable") {
t.Errorf("the unknown field was not named: %v", refusal.Problems)
}
}
func TestAFieldTheTypeDoesNotUseIsRefusedNotIgnored(t *testing.T) {
// The same fault in miniature: set and ignored means the control plane believes it asked
// for something the host will never do.
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"svc","type":"service","unit":"a.service","state":"running","path":"/etc/x"}
]}`)
joined := strings.Join(refusal.Problems, "\n")
if !strings.Contains(joined, "path") || !strings.Contains(joined, "Refused rather than ignored") {
t.Errorf("a field a service does not use was accepted: %v", refusal.Problems)
}
}
func TestAnUnknownVersionIsRefusedWhole(t *testing.T) {
// An older host handed a newer vocabulary must not quietly do half of it.
refusal := refusalFor(t, `{"declaration":99,"resources":[
{"id":"a","type":"directory","path":"/etc/mesh"}
]}`)
joined := strings.Join(refusal.Problems, "\n")
if !strings.Contains(joined, "99") || !strings.Contains(joined, "version 1") {
t.Errorf("the version mismatch was not stated plainly: %v", refusal.Problems)
}
// Nothing else is reported, because everything else assumes a vocabulary this host does
// not have — a list of complaints derived from the wrong grammar is noise.
if len(refusal.Problems) != 1 {
t.Errorf("expected only the version problem, got: %v", refusal.Problems)
}
}
func TestEveryProblemIsReportedAtOnce(t *testing.T) {
// A declaration is generated, so a person reading a refusal is debugging the generator.
// Fixing one problem at a time and re-running to find the next wastes their afternoon.
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"","type":"directory","path":"/a"},
{"id":"b","type":"file"},
{"id":"c","type":"service","unit":"x.service","state":"dancing"}
]}`)
if len(refusal.Problems) < 3 {
t.Errorf("expected every problem at once, got: %v", refusal.Problems)
}
}
func TestIdentityIsRequiredAndUnique(t *testing.T) {
// Identity is what lets the store know this is the same resource it applied last time,
// which is what makes removal possible at all.
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"same","type":"directory","path":"/a"},
{"id":"same","type":"directory","path":"/b"}
]}`)
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "already used") {
t.Errorf("a duplicate identity was accepted: %v", refusal.Problems)
}
}
func TestModeIsRefusedUnlessItMeansWhatItLooksLike(t *testing.T) {
// "644" and "0644" differ, and the one that looks right in a manifest is the four-digit
// form. Accepting both would make a mode mean two things.
for _, mode := range []string{"644", "0999", "rwxr-xr-x", "07777777"} {
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"f","type":"file","path":"/a","mode":"`+mode+`"}
]}`)
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "mode") {
t.Errorf("mode %q was accepted: %v", mode, refusal.Problems)
}
}
if _, err := Parse([]byte(`{"declaration":1,"resources":[
{"id":"f","type":"file","path":"/a","mode":"0644"}
]}`)); err != nil {
t.Errorf("a well-formed mode was refused: %v", err)
}
}
func TestARefusalSaysNothingWasApplied(t *testing.T) {
// The reader's first question is whether the machine was left half-changed.
refusal := refusalFor(t, `{"declaration":1,"resources":[{"id":"x","type":"nope"}]}`)
if !strings.Contains(refusal.Error(), "none of it was applied") {
t.Errorf("the refusal does not say the machine is untouched: %s", refusal.Error())
}
}
func TestAnEmptyDeclarationIsAMistake(t *testing.T) {
refusalFor(t, `{"declaration":1,"resources":[]}`)
}
// --- the vocabulary the substrate bootstrap needs (novox/hq 07-the-substrate.md) ---
func TestAnActionOverTheLinkIsRefused(t *testing.T) {
// novox/hq ADR 0005. The link may push declarations of known shape and never a command to
// run. This is the boundary the whole security argument rests on, so it is asserted
// directly rather than inferred from the type list.
raw := []byte(`{"declaration":1,"resources":[
{"id":"schema","type":"action","command":["psql","-f","x.sql"],"verify":["psql","-c","select 1"]}
]}`)
if _, err := Parse(raw); err == nil {
t.Fatal("an action arriving over the link was accepted")
} else if !strings.Contains(err.Error(), "the link may not carry one") {
t.Errorf("refused for the wrong reason: %v", err)
}
// And the same bytes from the bundle are fine — the asymmetry IS the decision.
if _, err := ParseTrusted(raw); err != nil {
t.Errorf("the bundle may carry an action, and this one was refused: %v", err)
}
}
func TestAnActionWithoutVerifyIsRefused(t *testing.T) {
// An action that runs and reports success without reading anything back is the fault this
// host exists to prevent. Verify is also the idempotency check, so an action without one
// cannot be applied twice safely either.
_, err := ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"schema","type":"action","command":["psql","-f","x.sql"]}
]}`))
if err == nil {
t.Fatal("an action with no verify was accepted")
}
if !strings.Contains(err.Error(), "needs a verify") {
t.Errorf("refused for the wrong reason: %v", err)
}
}
func TestAnImageMustBePinnedByDigest(t *testing.T) {
// novox/hq ADR 0006: reproducibility comes from pinning the identity of a thing. A bundle
// naming a tag pins nothing — it names whatever that tag points at on the day it runs.
for _, image := range []string{
"postgres:17",
"postgres",
"postgres@sha256:short",
"@sha256:0000000000000000000000000000000000000000000000000000000000000000",
} {
_, err := ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"` + image + `"}
]}`))
if err == nil {
t.Errorf("image %q was accepted and is not pinned", image)
}
}
good := "postgres@sha256:" + strings.Repeat("a", 64)
if _, err := ParseTrusted([]byte(`{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"` + good + `"}
]}`)); err != nil {
t.Errorf("a properly pinned image was refused: %v", err)
}
}
func TestAFieldTheNewTypesDoNotUseIsRefused(t *testing.T) {
// The field-set check must cover the types added last, not only the three it was written
// for. A package that carries a `content` is a control plane believing it asked for
// something that will never happen.
for _, body := range []string{
`{"id":"p","type":"package","package":"docker","content":"x"}`,
`{"id":"p","type":"package","package":"docker","image":"x"}`,
`{"id":"c","type":"container","name":"n","image":"i@sha256:` + strings.Repeat("a", 64) + `","unit":"x.service"}`,
`{"id":"a","type":"action","command":["x"],"verify":["y"],"path":"/tmp/x"}`,
} {
_, err := ParseTrusted([]byte(`{"declaration":1,"resources":[` + body + `]}`))
if err == nil {
t.Errorf("a resource carrying a field its type does not use was accepted: %s", body)
continue
}
if !strings.Contains(err.Error(), "Refused rather than ignored") {
t.Errorf("refused for the wrong reason: %v", err)
}
}
}
func TestTheVocabularyIsTheEightShapesTheMeshNeeds(t *testing.T) {
// Six of them the bootstrap uses (novox/hq 07-the-substrate.md), and removing one is a
// failing test rather than a discovery during a first-node install.
//
// Two were added on 2026-08-30 and the count is asserted precisely because adding one is a
// decision. `user` and `archive` exist because most of what a person installs is not a
// service: a shell, a chat client, a desktop are a package plus configuration **in
// somebody's home**, and a mesh with no user can only own /etc. `archive` is for the case
// inlining cannot serve — a theme is hundreds of files, and inlining them would rewrite all
// of them whenever one changed.
speaks := map[Type]bool{}
for _, t := range Vocabulary() {
speaks[t] = true
}
for _, want := range []Type{
TypeDirectory, TypeFile, TypeService, TypePackage, TypeContainer, TypeAction,
TypeUser, TypeArchive, TypeNetwork,
} {
if !speaks[want] {
t.Errorf("the host no longer speaks %q", want)
}
if newOf(want) == nil {
t.Errorf("%q is in the vocabulary and cannot be constructed", want)
}
}
// `network` is the ninth, and novox/hq ADR 0029 is the decision that made it one: an action
// could create a network and nothing could remove it, because an action leaves no footprint
// the host can undo — so the network would outlive every module that was ever unassigned.
if len(speaks) != 9 {
t.Errorf("the vocabulary is %d shapes rather than 9; every addition widens what a compromised "+
"control plane can express, so a change here is a decision: %s",
len(speaks), vocabulary())
}
}
func TestADeclarationFromDiskMayBeAnnotated(t *testing.T) {
// The bundle in this repository is mostly explanation of why each digest is what it is, and
// it could be built into a binary and not applied from disk — two readers for one file. The
// failure was `invalid character '/'`, which names the symptom and not the cause.
raw := []byte(`// why this exists
{
"declaration": 1,
// and why this resource is here
"resources": [
{"id": "f", "type": "file", "path": "/etc/x", "content": "hello"}
]
}`)
d, err := ParseFileTrusted(raw)
if err != nil {
t.Fatalf("a file with comments was refused: %v", err)
}
if len(d.Resources) != 1 {
t.Fatalf("got %d resources", len(d.Resources))
}
// And the wire format is untouched: over the link it is exactly JSON, because a format with
// a second thing to strip is a format with a second thing to disagree about.
if _, err := Parse(raw); err == nil {
t.Fatal("the link accepted a declaration with comments in it")
}
}
func TestSomethingInsideAValueIsNotAComment(t *testing.T) {
// Every image reference has a `//` in it somewhere near. Only whole lines are dropped.
d, err := ParseFileTrusted([]byte(`{"declaration":1,"resources":[
{"id":"f","type":"file","path":"/etc/x","content":"see https://example.invalid/ for why"}]}`))
if err != nil {
t.Fatal(err)
}
file, ok := d.Resources[0].(*File)
if !ok {
t.Fatalf("got %T", d.Resources[0])
}
if !strings.Contains(file.Content, "https://example.invalid/") {
t.Fatalf("a value was mangled: %q", file.Content)
}
}
// A declaration followed by anything at all is refused whole.
//
// A JSON decoder reads one value and stops, so a file holding a declaration and then a stray line
// parses as the declaration and the rest is never looked at. The machine applies something,
// reports success, and what it applied is not what the file says.
//
// Not hypothetical: a test harness appended a line to the substrate bundle by accident, every
// apply kept working, and nothing said so for the entire time it was wrong.
func TestSomethingAfterTheDeclarationIsRefused(t *testing.T) {
good := `{"declaration":1,"resources":[{"id":"a","type":"file","path":"/tmp/a",` +
`"content":"x","mode":"0644"}]}`
if _, err := ParseFileTrusted([]byte(good)); err != nil {
t.Fatalf("an ordinary declaration was refused: %v", err)
}
for _, after := range []string{
"MESHBUNDLE 2>&1; echo \"__exit=$?\"",
good,
"garbage",
} {
_, err := ParseFileTrusted([]byte(good + "\n" + after))
if err == nil {
t.Fatalf("a file with %q after the declaration was accepted", after)
}
}
// Trailing whitespace is not "something after it", and refusing it would make every file
// written by an editor unusable.
if _, err := ParseFileTrusted([]byte(good + "\n\n \n")); err != nil {
t.Fatalf("a declaration with a trailing newline was refused: %v", err)
}
}
// A name that would read as a reference to something else is refused before it reaches a runtime.
func TestANetworkNameIsRefusedIfItIsNotOne(t *testing.T) {
for _, name := range []string{"", "mail/private", "host:mail", "a b"} {
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"private","type":"network","name":"`+name+`"}
]}`)
if len(refusal.Problems) == 0 {
t.Errorf("a network named %q was accepted", name)
}
}
}
// A placeholder with nothing to fill it is refused before anything is written.
//
// The alternative is a configuration file containing the literal `${secret:x}`, which the program
// reads as a value and fails on somewhere with no connection to this.
func TestAFileAskingForASecretItWasNotGivenIsRefused(t *testing.T) {
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"c","type":"file","path":"/tmp/x","content":"token=${secret:absent}"}
]}`)
if len(refusal.Problems) == 0 {
t.Fatal("a file naming a secret nobody gave it was accepted")
}
}
// And a secret nobody asked for, because somebody believes it is in a file where it is not.
func TestASecretTheContentNeverUsesIsRefused(t *testing.T) {
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"c","type":"file","path":"/tmp/x","content":"nothing here","secrets":{"spare":"S"}}
]}`)
if len(refusal.Problems) == 0 {
t.Fatal("a secret the content never mentions was accepted")
}
}