package apply import ( "strings" "testing" "time" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/store" ) // Defends novox/hq issue 104: what an apply would change is said before anything is, from the // declaration and the node's record — and an action is named as the action it is. func trusted(t *testing.T, raw string) *declaration.Declaration { t.Helper() d, err := declaration.ParseFileTrusted([]byte(raw)) if err != nil { t.Fatalf("fixture is not a valid declaration: %v", err) } return d } func verbs(steps []Step) string { var out []string for _, s := range steps { out = append(out, s.Verb+" "+s.ID) } return strings.Join(out, ", ") } func TestAPlanNamesAnActionAsAnAction(t *testing.T) { d := trusted(t, `{"declaration":1,"resources":[ {"id":"init","type":"action","command":["createdb","mesh"],"verify":["psql","-c","select 1"]}]}`) steps := Plan(d, store.State{}, store.OriginCarried) if len(steps) != 1 || steps[0].Verb != "run" { t.Fatalf("an action was planned as %s", verbs(steps)) } if !strings.Contains(steps[0].Why, "createdb mesh") || !strings.Contains(steps[0].Why, "nothing after it") { t.Errorf("the plan does not say what the action runs and what failing it means: %q", steps[0].Why) } } func TestAPlanSaysWhatIsRecordedAndWhatIsNot(t *testing.T) { d := parse(t, `{"declaration":1,"resources":[ {"id":"new","type":"file","path":"/tmp/new","content":"a\n"}, {"id":"same","type":"file","path":"/tmp/same","content":"b\n"}, {"id":"moved","type":"file","path":"/tmp/moved","content":"c\n"}, {"id":"sealed","type":"file","path":"/tmp/sealed","sealed":"AAAA","mode":"0600"}]}`) known := store.State{} known.Record(store.Applied{ID: "same", Type: "file", Target: "/tmp/same", Wrote: digestOf("b\n")}) known.Record(store.Applied{ID: "moved", Type: "file", Target: "/tmp/moved", Wrote: digestOf("old\n")}) known.Record(store.Applied{ID: "sealed", Type: "file", Target: "/tmp/sealed", Wrote: digestOf("secret")}) known.Record(store.Applied{ID: "gone", Type: "file", Target: "/tmp/gone"}) got := verbs(Plan(d, known, store.OriginCarried)) want := "remove gone, create new, check same, update moved, check sealed" if got != want { t.Errorf("planned %q, want %q", got, want) } } func TestAPlanSaysTheFirewallAConvergingNodeRetires(t *testing.T) { d := parse(t, `{"declaration":1,"resources":[{"id":"a","type":"file","path":"/tmp/a","content":"x\n"}]}`) known := store.State{Firewall: &store.FoundFirewall{Kind: "ufw", WasActive: true, FoundAt: time.Now()}} known.Record(store.Applied{ID: "adoption.guard.table", Type: "file", Target: "/etc/guard", Origin: store.OriginDeclared}) got := verbs(Plan(d, known, store.OriginDeclared)) // The firewall goes last but for what protected the node, which goes after it. if got != "create a, disable ufw, remove adoption.guard.table" { t.Errorf("a converging node planned %q", got) } // A file or the bundle never retires the firewall found here, and says nothing about it. if got := verbs(Plan(d, known, store.OriginCarried)); strings.Contains(got, "ufw") { t.Errorf("a carried declaration planned to touch the firewall: %q", got) } } func TestAPlanHoldsWhatAnAdoptedNodeFound(t *testing.T) { d := parse(t, `{"declaration":1,"adoption":{"taken":[],"untaken":{"hello-web":["hello-web.page","hello-web.server"]}}, "resources":[ {"id":"hello-web.page","type":"file","path":"/srv/index.html","content":"x\n"}, {"id":"hello-web.server","type":"container","name":"hello-web","image":"example/web@sha256:0000000000000000000000000000000000000000000000000000000000000000"}]}`) known := store.State{} known.RecordHeld(store.Held{ID: "hello-web.page", Module: "hello-web", Kind: "file", Target: "/srv/index.html"}) steps := Plan(d, known, store.OriginDeclared) if len(steps) != 2 || steps[0].Verb != "hold" || steps[1].Verb != "create" { t.Fatalf("an adopted node planned %s", verbs(steps)) } if !strings.Contains(steps[1].Why, "held as it is until hello-web is taken") { t.Errorf("the plan does not say an untaken module's resource is held if found: %q", steps[1].Why) } }