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":[]}`) }