package apply import ( "strings" "testing" "github.com/novox/mesh-host/internal/declaration" ) func aProcess() *declaration.Process { return &declaration.Process{ ID: "server", Type: declaration.TypeProcess, Name: "greeter", Source: "https://store.invalid/greeter/daemon", Digest: "sha256:" + strings.Repeat("a", 64), Run: []string{"node", "index.js"}, Env: map[string]string{"MESH_NODE": "anchor", "A_FIRST": "1"}, } } // The unit the mesh writes says what it runs, where, and that it comes back. func TestTheUnitRunsWhatTheDaemonSaid(t *testing.T) { unit := unitFor(aProcess()) for _, want := range []string{ "ExecStart=node index.js", "WorkingDirectory=/var/lib/mesh/daemons/greeter", "Restart=always", "WantedBy=multi-user.target", } { if !strings.Contains(unit, want) { t.Fatalf("the unit does not say %q:\n%s", want, unit) } } } // **Generated whole and saying so.** Every managed file on a machine carries this, because an edit // that survives until the next declaration and then vanishes is worse than one that is refused. func TestTheUnitSaysItIsTheMeshs(t *testing.T) { unit := unitFor(aProcess()) if !strings.HasPrefix(unit, "#") || !strings.Contains(unit, "Do not edit") { t.Fatalf("the unit does not say it is generated:\n%s", unit) } } // **Deterministic, because the unit is half the daemon's identity.** Environment held in a map // would be written in Go's iteration order, so every apply would see a different unit and call an // unchanged daemon changed — restarting it on every declaration for ever. func TestTheUnitIsTheSameEveryTime(t *testing.T) { first := unitFor(aProcess()) for i := 0; i < 20; i++ { if again := unitFor(aProcess()); again != first { t.Fatalf("two renderings of one daemon differ:\n%s\n---\n%s", first, again) } } // And sorted, so the order is a decision rather than luck. if strings.Index(first, "A_FIRST") > strings.Index(first, "MESH_NODE") { t.Fatalf("environment is not in a stable order:\n%s", first) } } // **Two daemons from one bundle differing only in their command are different daemons.** Tracking // the digest alone would call the second one unchanged and leave the first one running. func TestAProcesssIdentityIncludesHowItIsRun(t *testing.T) { one := aProcess() two := aProcess() two.Run = []string{"node", "other.js"} if unitFor(one) == unitFor(two) { t.Fatal("two daemons with different commands render one unit, so a change would be missed") } } // A process that runs as somebody says so, and one that does not says nothing — rather than naming // root explicitly, which would be a claim the mesh does not need to make. func TestAProcessRunsAsWhoItSaid(t *testing.T) { as := aProcess() as.User = "greeter" if !strings.Contains(unitFor(as), "User=greeter") { t.Fatalf("the unit does not run as the user it named:\n%s", unitFor(as)) } if strings.Contains(unitFor(aProcess()), "User=") { t.Fatalf("a process that named no user had one written for it:\n%s", unitFor(aProcess())) } } // **Three modes, one kind.** A scheduled process is a timer plus a unit that finishes, not a unit // that stays up — and the difference has to be in what is written, or a schedule becomes a second // copy running continuously between fires. func TestAScheduledProcessRunsOnItsCadenceRatherThanContinuously(t *testing.T) { every := aProcess() every.Schedule = "0 3 * * *" unit := unitFor(every) if strings.Contains(unit, "Restart=always") { t.Fatalf("a scheduled process is restarted whenever it exits, so it never stops:\n%s", unit) } if !strings.Contains(unit, "Type=oneshot") { t.Fatalf("a scheduled process is not a step that finishes:\n%s", unit) } timer := timerFor(every) if !strings.Contains(timer, "OnCalendar=") { t.Fatalf("a scheduled process has no cadence:\n%s", timer) } // A fire missed while the machine was off happens when it returns, rather than being skipped — // the difference between a machine that was down and a schedule that quietly stopped. if !strings.Contains(timer, "Persistent=true") { t.Fatalf("a missed fire is skipped silently:\n%s", timer) } } // Five-field cron becomes what a timer reads, rather than the host waking to decide. func TestACronBecomesATimersCalendar(t *testing.T) { for cron, want := range map[string]string{ "0 3 * * *": "*-*-* 3:0:00", "30 4 1 * *": "*-*-1 4:30:00", "0 0 * * mon": "mon *-*-* 0:0:00", } { if got := calendarFor(cron); got != want { t.Fatalf("%q became %q rather than %q", cron, got, want) } } } // And the long-running mode is unchanged by any of it: it stays up and comes back. func TestAProcessThatStaysUpIsStillRestartedWhenItExits(t *testing.T) { unit := unitFor(aProcess()) if !strings.Contains(unit, "Restart=always") { t.Fatalf("a process that should stay up is not restarted when it exits:\n%s", unit) } if strings.Contains(unit, "Type=oneshot") { t.Fatalf("a process that should stay up is declared a step:\n%s", unit) } }