User templates can now be added without rebuilding: drop a script into ~/.config/motd-assist/templates/ and reference it from a preset via "install": "<filename>". Same-name files override built-ins; new files are picked up by the running TUI via 'r'. `motd-assist templates` lists known templates and creates the drop-in directory. Apply plans now embed the resolved template content (Plan.ResolveContents) so the sudo/root process never needs to read user config; snapshots store installed script content so restore works under sudo as well. Validation rejects plans with unresolved content.
100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
package motd
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// setupDir legt ein update-motd.d-Verzeichnis unter einem Temp-Root an.
|
|
func setupDir(t *testing.T, files map[string]string) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
dir := MotdDir(root)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, content := range files {
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return root
|
|
}
|
|
|
|
func TestValidateRejectsUnknownNames(t *testing.T) {
|
|
root := setupDir(t, map[string]string{"00-header": "#!/bin/sh\necho hi\n"})
|
|
|
|
err := Validate(root, Plan{Chmod: []ChmodOp{{Name: "../evil", Enable: true}}})
|
|
if err == nil {
|
|
t.Error("Pfad-Traversal sollte abgelehnt werden")
|
|
}
|
|
err = Validate(root, Plan{Chmod: []ChmodOp{{Name: "gibt-es-nicht", Enable: true}}})
|
|
if err == nil {
|
|
t.Error("chmod auf fehlende Datei sollte abgelehnt werden")
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsForeignOverwrite(t *testing.T) {
|
|
root := setupDir(t, map[string]string{"00-header": "#!/bin/sh\necho eigenes\n"})
|
|
|
|
err := Validate(root, Plan{Installs: []InstallOp{{
|
|
Name: "00-header", Source: "footer", Enabled: true,
|
|
Content: "#!/bin/sh\necho neu\n",
|
|
}}})
|
|
if err == nil {
|
|
t.Error("Install über fremdes Script sollte abgelehnt werden")
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsEmptyInstallContent(t *testing.T) {
|
|
root := setupDir(t, nil)
|
|
|
|
err := Validate(root, Plan{Installs: []InstallOp{{Name: "10-x", Source: "unbekannt", Enabled: true}}})
|
|
if err == nil {
|
|
t.Error("Install ohne aufgelösten Inhalt sollte abgelehnt werden")
|
|
}
|
|
}
|
|
|
|
func TestResolveContentsEmbedsTemplates(t *testing.T) {
|
|
plan := Plan{Installs: []InstallOp{{Name: "10-x", Source: "brand-header", Enabled: true}}}
|
|
if err := plan.ResolveContents(); err != nil {
|
|
t.Fatalf("ResolveContents: %v", err)
|
|
}
|
|
if plan.Installs[0].Content == "" {
|
|
t.Error("Content wurde nicht befüllt")
|
|
}
|
|
|
|
missing := Plan{Installs: []InstallOp{{Name: "10-y", Source: "gibt-es-nicht", Enabled: true}}}
|
|
if err := missing.ResolveContents(); err == nil {
|
|
t.Error("unbekannte Vorlage sollte einen Fehler liefern")
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsForeignRemove(t *testing.T) {
|
|
root := setupDir(t, map[string]string{"10-sysinfo": "#!/bin/sh\necho eigenes\n"})
|
|
|
|
err := Validate(root, Plan{Removes: []RemoveOp{{Name: "10-sysinfo"}}})
|
|
if err == nil {
|
|
t.Error("Remove eines fremden Scripts sollte abgelehnt werden")
|
|
}
|
|
}
|
|
|
|
func TestValidateRenameCollision(t *testing.T) {
|
|
root := setupDir(t, map[string]string{
|
|
"00-a": "#!/bin/sh\n",
|
|
"10-b": "#!/bin/sh\n",
|
|
})
|
|
|
|
// b wird nicht wegbewegt → Ziel 10-b ist belegt.
|
|
err := Validate(root, Plan{Renames: []RenameOp{{From: "00-a", To: "10-b"}}})
|
|
if err == nil {
|
|
t.Error("Kollision sollte abgelehnt werden")
|
|
}
|
|
|
|
// Beide tauschen → zweiphasige Ausführung macht das frei.
|
|
err = Validate(root, Plan{Renames: []RenameOp{{From: "00-a", To: "10-b"}, {From: "10-b", To: "00-a"}}})
|
|
if err != nil {
|
|
t.Errorf("Tausch sollte erlaubt sein: %v", err)
|
|
}
|
|
}
|