148 lines
5.0 KiB
Go
148 lines
5.0 KiB
Go
package main
|
|
|
|
// lemurs's shape (novox/hq ADR 0208): it holds node-login-manager; it owns the configuration in
|
|
// lemurs 0.4's format and enables the service without ever starting, stopping or restarting it,
|
|
// because the running login manager is the operator's way in.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Capabilities []string `json:"capabilities"`
|
|
Claims []struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Serves []string `json:"serves"`
|
|
} `json:"claims"`
|
|
Seats any `json:"seats"`
|
|
Tools []string `json:"tools"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []map[string]any `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
func readManifest(t *testing.T) manifest {
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func resource(t *testing.T, m manifest, id string) map[string]any {
|
|
for _, r := range m.Resources {
|
|
if r["id"] == id {
|
|
return r
|
|
}
|
|
}
|
|
t.Fatalf("no resource %s", id)
|
|
return nil
|
|
}
|
|
|
|
func TestItHoldsTheLoginManagerSeatAndServesSessions(t *testing.T) {
|
|
m := readManifest(t)
|
|
if m.Seats != nil || len(m.Claims) != 1 || m.Claims[0].Name != "node-login-manager" || strings.Join(m.Claims[0].Serves, ",") != "sessions" {
|
|
t.Fatalf("%+v", m.Claims)
|
|
}
|
|
served := map[string]bool{}
|
|
for _, tool := range tools(lemurs{}) {
|
|
served[tool.Name] = true
|
|
}
|
|
if !served["node-login-manager.sessions"] || len(served) != 1+len(m.Tools) {
|
|
t.Fatalf("served %v, manifest %v", served, m.Tools)
|
|
}
|
|
for _, name := range m.Tools {
|
|
if !served[name] {
|
|
t.Errorf("%s is listed and not served", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheOfficialPackageAndItsServiceEnabledButNeverRestarted(t *testing.T) {
|
|
m := readManifest(t)
|
|
if p := resource(t, m, "package"); p["package"] != "lemurs" {
|
|
t.Fatalf("the official package, not lemurs-git: %v", p)
|
|
}
|
|
s := resource(t, m, "service")
|
|
// Running and enabled: the host refuses boot without a state (a stateless service declares only
|
|
// its triggers). Running starts it only where it is not; with no trigger, nothing restarts it.
|
|
if s["unit"] != "lemurs.service" || s["boot"] != "enabled" || s["state"] != "running" {
|
|
t.Fatalf("%v", s)
|
|
}
|
|
for _, k := range []string{"restart-on", "reload-on"} {
|
|
if _, ok := s[k]; ok {
|
|
t.Fatalf("the login manager is never restarted by a push (%s): a change applies at its next start", k)
|
|
}
|
|
}
|
|
if strings.Join(m.Capabilities, ",") != "package-manager,service-manager,seat" {
|
|
t.Fatalf("%v", m.Capabilities)
|
|
}
|
|
}
|
|
|
|
func TestTheConfigurationIsTheModulesFileAndOffersOnlyTheSessionsModulesPlace(t *testing.T) {
|
|
m := readManifest(t)
|
|
c := resource(t, m, "config")
|
|
raw, _ := os.ReadFile(filepath.Join("..", "..", "files", "config.toml"))
|
|
if _, inline := c["content"]; inline || c["template"] != "files/config.toml" || len(raw) == 0 ||
|
|
c["path"] != "/etc/lemurs/config.toml" || c["into"] != nil {
|
|
t.Fatal("the manifest takes files/config.toml whole as its template, as an owned file")
|
|
}
|
|
text := string(raw)
|
|
for _, want := range []string{"[x11]", "[wayland]", `xsetup_path = "/etc/lemurs/xsetup.sh"`, `scripts_path = "/etc/lemurs/wms"`,
|
|
`scripts_path = "/etc/lemurs/wayland"`, `xsessions_path = "/etc/lemurs/xsessions"`, `wayland_sessions_path = "/etc/lemurs/wayland-sessions"`,
|
|
`tty = 2`, `x11_display = ":1"`, `remember = true`} {
|
|
if !strings.Contains(text, want) {
|
|
t.Errorf("the configuration lacks %s", want)
|
|
}
|
|
}
|
|
for _, l := range strings.Split(text, "\n") {
|
|
if (strings.HasPrefix(l, "xsessions_path") || strings.HasPrefix(l, "wayland_sessions_path")) && strings.Contains(l, "/usr/share") {
|
|
t.Fatalf("a package's bare desktop entry would be offered: %s", l)
|
|
}
|
|
}
|
|
for _, id := range []string{"xsessions", "wayland-sessions"} {
|
|
if d := resource(t, m, id); d["type"] != "directory" || d["path"] != "/etc/lemurs/"+id {
|
|
t.Fatalf("%v", d)
|
|
}
|
|
}
|
|
}
|
|
|
|
// No file resource carries its text inline: each names its template in files/, and the build packs files/
|
|
// into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
|
|
func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
|
|
m := readManifest(t)
|
|
for _, r := range m.Resources {
|
|
if _, inline := r["content"]; inline {
|
|
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
|
|
}
|
|
if name, ok := r["template"].(string); ok {
|
|
if _, err := os.Stat(filepath.Join("..", "..", name)); err != nil {
|
|
t.Errorf("resource %v names %s: %v", r["id"], name, err)
|
|
}
|
|
}
|
|
}
|
|
packs := 0
|
|
for _, a := range m.Build.Artifacts {
|
|
if a["kind"] == "archive" && a["from"] == "files" {
|
|
packs++
|
|
if a["name"] != "files" {
|
|
t.Errorf("the templates archive is named %v, not files", a["name"])
|
|
}
|
|
}
|
|
}
|
|
if packs != 1 {
|
|
t.Errorf("the build packs files/ into %d archives, not one", packs)
|
|
}
|
|
}
|