package declaration import ( "errors" "strings" "testing" ) // Each test names the decision it defends (novox/hq ADR 0034). The decision here is ADR 0043, // 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].ID, d.Resources[1].ID, d.Resources[2].ID} 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","owner":"root"} ]}`) if !strings.Contains(strings.Join(refusal.Problems, "\n"), "owner") { 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 0047. 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 0046: 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 TestTheVocabularyIsTheSixShapesTheBootstrapNeeds(t *testing.T) { // novox/hq 07-the-substrate.md names six shapes and the bootstrap uses all of them. // Asserted so that removing one is a failing test rather than a discovery during a // first-node install. for _, want := range []Type{ TypeDirectory, TypeFile, TypeService, TypePackage, TypeContainer, TypeAction, } { if _, ok := uses[want]; !ok { t.Errorf("the host no longer speaks %q", want) } } if len(uses) != 6 { t.Errorf("the vocabulary is %d shapes; every addition widens what a compromised "+ "control plane can express, so a change here is a decision: %s", len(uses), vocabulary()) } }