325 lines
9.5 KiB
Go
325 lines
9.5 KiB
Go
package launcher
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRenderSystemdUnitExecsNativeKandev(t *testing.T) {
|
|
unit := renderSystemdUnit(nativeServiceUnitInput{
|
|
Executable: "/opt/kandev/bin/kandev",
|
|
HomeDir: "/home/alice/.kandev",
|
|
LogDir: "/home/alice/.kandev/logs",
|
|
Port: 1234,
|
|
})
|
|
if !strings.Contains(unit, "ExecStart=/opt/kandev/bin/kandev --headless") {
|
|
t.Fatalf("unit does not exec native kandev:\n%s", unit)
|
|
}
|
|
if strings.Contains(unit, "cli.js") || strings.Contains(unit, "node ") {
|
|
t.Fatalf("unit contains Node CLI launcher:\n%s", unit)
|
|
}
|
|
if !strings.Contains(unit, "Environment=KANDEV_SERVER_PORT=1234") {
|
|
t.Fatalf("unit missing port env:\n%s", unit)
|
|
}
|
|
}
|
|
|
|
func TestRenderSystemdUnitIncludesBundleMetadata(t *testing.T) {
|
|
unit := renderSystemdUnit(nativeServiceUnitInput{
|
|
Executable: "/opt/kandev/bin/kandev",
|
|
HomeDir: "/home/alice/.kandev",
|
|
LogDir: "/home/alice/.kandev/logs",
|
|
BundleDir: "/opt/kandev",
|
|
Version: "1.2.3",
|
|
})
|
|
|
|
if !strings.Contains(unit, "Environment=KANDEV_BUNDLE_DIR=/opt/kandev") {
|
|
t.Fatalf("unit missing bundle dir env:\n%s", unit)
|
|
}
|
|
if !strings.Contains(unit, "Environment=KANDEV_VERSION=1.2.3") {
|
|
t.Fatalf("unit missing version env:\n%s", unit)
|
|
}
|
|
}
|
|
|
|
func TestRenderLaunchdPlistExecsNativeKandev(t *testing.T) {
|
|
plist := renderLaunchdPlist(nativeServiceUnitInput{
|
|
Executable: "/opt/kandev/bin/kandev",
|
|
HomeDir: "/Users/alice/.kandev",
|
|
LogDir: "/Users/alice/.kandev/logs",
|
|
})
|
|
if !strings.Contains(plist, "<string>/opt/kandev/bin/kandev</string>") {
|
|
t.Fatalf("plist does not exec native kandev:\n%s", plist)
|
|
}
|
|
if strings.Contains(plist, "cli.js") {
|
|
t.Fatalf("plist contains Node CLI launcher:\n%s", plist)
|
|
}
|
|
if !strings.Contains(plist, "<key>RunAtLoad</key>\n <true/>") {
|
|
t.Fatalf("plist should start at load by default:\n%s", plist)
|
|
}
|
|
}
|
|
|
|
func TestRenderLaunchdPlistIncludesBundleMetadata(t *testing.T) {
|
|
plist := renderLaunchdPlist(nativeServiceUnitInput{
|
|
Executable: "/opt/kandev/bin/kandev",
|
|
HomeDir: "/Users/alice/.kandev",
|
|
LogDir: "/Users/alice/.kandev/logs",
|
|
BundleDir: "/opt/kandev",
|
|
Version: "1.2.3",
|
|
})
|
|
|
|
if !strings.Contains(plist, "<key>KANDEV_BUNDLE_DIR</key>\n <string>/opt/kandev</string>") {
|
|
t.Fatalf("plist missing bundle dir env:\n%s", plist)
|
|
}
|
|
if !strings.Contains(plist, "<key>KANDEV_VERSION</key>\n <string>1.2.3</string>") {
|
|
t.Fatalf("plist missing version env:\n%s", plist)
|
|
}
|
|
}
|
|
|
|
func TestRenderLaunchdPlistCanDisableBootStart(t *testing.T) {
|
|
plist := renderLaunchdPlist(nativeServiceUnitInput{
|
|
Executable: "/opt/kandev/bin/kandev",
|
|
HomeDir: "/Users/alice/.kandev",
|
|
LogDir: "/Users/alice/.kandev/logs",
|
|
NoBootStart: true,
|
|
})
|
|
|
|
if !strings.Contains(plist, "<key>RunAtLoad</key>\n <false/>") {
|
|
t.Fatalf("plist should not start at load with no boot start:\n%s", plist)
|
|
}
|
|
}
|
|
|
|
func TestInstallSystemdMessagesDistinguishBootStart(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
noBootStart bool
|
|
wantMessage string
|
|
wantEnableNow bool
|
|
wantStartAction bool
|
|
}{
|
|
{
|
|
name: "default install",
|
|
wantMessage: "[kandev] service enabled and started",
|
|
wantEnableNow: true,
|
|
},
|
|
{
|
|
name: "no boot start",
|
|
noBootStart: true,
|
|
wantMessage: "[kandev] service installed and started (boot-start disabled)",
|
|
wantStartAction: true,
|
|
},
|
|
}
|
|
|
|
originalExecutablePath := executablePath
|
|
originalExecuteServiceCommand := executeServiceCommand
|
|
originalServicePrintln := servicePrintln
|
|
t.Cleanup(func() {
|
|
executablePath = originalExecutablePath
|
|
executeServiceCommand = originalExecuteServiceCommand
|
|
servicePrintln = originalServicePrintln
|
|
})
|
|
|
|
executablePath = func() (string, error) {
|
|
return "/opt/kandev/bin/kandev", nil
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var commands []string
|
|
var messages []string
|
|
executeServiceCommand = func(name string, args ...string) error {
|
|
commands = append(commands, name+" "+strings.Join(args, " "))
|
|
return nil
|
|
}
|
|
servicePrintln = func(message string) {
|
|
messages = append(messages, message)
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
code := installSystemd(
|
|
serviceArgs{
|
|
Action: actionInstall,
|
|
HomeDir: filepath.Join(tmp, "home"),
|
|
NoBootStart: tt.noBootStart,
|
|
},
|
|
BuildInfo{Version: "test"},
|
|
filepath.Join(tmp, "kandev.service"),
|
|
)
|
|
if code != 0 {
|
|
t.Fatalf("installSystemd() = %d, want 0", code)
|
|
}
|
|
if !slices.Equal(messages, []string{tt.wantMessage}) {
|
|
t.Fatalf("messages = %v, want %q", messages, tt.wantMessage)
|
|
}
|
|
gotEnableNow := slices.Contains(commands, "systemctl --user enable --now kandev.service")
|
|
if gotEnableNow != tt.wantEnableNow {
|
|
t.Fatalf("enable --now command presence = %v, want %v; commands=%v", gotEnableNow, tt.wantEnableNow, commands)
|
|
}
|
|
gotStartAction := slices.Contains(commands, "systemctl --user start kandev.service")
|
|
if gotStartAction != tt.wantStartAction {
|
|
t.Fatalf("start command presence = %v, want %v; commands=%v", gotStartAction, tt.wantStartAction, commands)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInstallLaunchdMessagesDistinguishBootStart(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
noBootStart bool
|
|
wantMessage string
|
|
wantKickstart bool
|
|
}{
|
|
{
|
|
name: "default install",
|
|
wantMessage: "[kandev] service loaded, enabled, and started",
|
|
wantKickstart: false,
|
|
},
|
|
{
|
|
name: "no boot start",
|
|
noBootStart: true,
|
|
wantMessage: "[kandev] service loaded and started (boot-start disabled)",
|
|
wantKickstart: true,
|
|
},
|
|
}
|
|
|
|
originalExecutablePath := executablePath
|
|
originalExecuteServiceCommand := executeServiceCommand
|
|
originalServicePrintln := servicePrintln
|
|
t.Cleanup(func() {
|
|
executablePath = originalExecutablePath
|
|
executeServiceCommand = originalExecuteServiceCommand
|
|
servicePrintln = originalServicePrintln
|
|
})
|
|
|
|
executablePath = func() (string, error) {
|
|
return "/opt/kandev/bin/kandev", nil
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var commands []string
|
|
var messages []string
|
|
executeServiceCommand = func(name string, args ...string) error {
|
|
commands = append(commands, name+" "+strings.Join(args, " "))
|
|
return nil
|
|
}
|
|
servicePrintln = func(message string) {
|
|
messages = append(messages, message)
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
code := installLaunchd(
|
|
serviceArgs{
|
|
Action: actionInstall,
|
|
HomeDir: filepath.Join(tmp, "home"),
|
|
NoBootStart: tt.noBootStart,
|
|
},
|
|
BuildInfo{Version: "test"},
|
|
filepath.Join(tmp, "com.kdlbs.kandev.plist"),
|
|
"gui/501/com.kdlbs.kandev",
|
|
"gui/501",
|
|
)
|
|
if code != 0 {
|
|
t.Fatalf("installLaunchd() = %d, want 0", code)
|
|
}
|
|
if !slices.Equal(messages, []string{tt.wantMessage}) {
|
|
t.Fatalf("messages = %v, want %q", messages, tt.wantMessage)
|
|
}
|
|
gotKickstart := slices.Contains(commands, "launchctl kickstart gui/501/com.kdlbs.kandev")
|
|
if gotKickstart != tt.wantKickstart {
|
|
t.Fatalf("kickstart command presence = %v, want %v; commands=%v", gotKickstart, tt.wantKickstart, commands)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildJournalArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args serviceArgs
|
|
want []string
|
|
}{
|
|
{
|
|
name: "user logs",
|
|
args: serviceArgs{Action: actionLogs},
|
|
want: []string{"--user-unit", "kandev.service", "-n", "200", "--no-pager"},
|
|
},
|
|
{
|
|
name: "user logs follow keeps line count",
|
|
args: serviceArgs{Action: actionLogs, Follow: true},
|
|
want: []string{"--user-unit", "kandev.service", "-n", "200", "-f"},
|
|
},
|
|
{
|
|
name: "system logs",
|
|
args: serviceArgs{Action: actionLogs, System: true},
|
|
want: []string{"-u", "kandev.service", "-n", "200", "--no-pager"},
|
|
},
|
|
{
|
|
name: "system logs follow keeps line count",
|
|
args: serviceArgs{Action: actionLogs, System: true, Follow: true},
|
|
want: []string{"-u", "kandev.service", "-n", "200", "-f"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := buildJournalArgs(tt.args)
|
|
if !slices.Equal(got, tt.want) {
|
|
t.Fatalf("buildJournalArgs() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQuoteForUnitEscapesSystemdSpecials(t *testing.T) {
|
|
got := serviceEnvLine("KANDEV_HOME_DIR", `/tmp/$USER/kandev%data`)
|
|
want := `Environment="KANDEV_HOME_DIR=/tmp/$$USER/kandev%%data"`
|
|
if got != want {
|
|
t.Fatalf("serviceEnvLine() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestServiceEnvLineAllowSpecifiersPreservesSystemdSpecifiers(t *testing.T) {
|
|
got := serviceEnvLineAllowSpecifiers("PATH", `%h/.local/bin:$PATH`)
|
|
want := `Environment="PATH=%h/.local/bin:$$PATH"`
|
|
if got != want {
|
|
t.Fatalf("serviceEnvLineAllowSpecifiers() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBackupUnmanagedServiceFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "kandev.service")
|
|
original := "custom unit"
|
|
if err := os.WriteFile(path, []byte(original), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := backupUnmanagedServiceFile(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, err := os.ReadFile(path + ".bak")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(data) != original {
|
|
t.Fatalf("backup = %q, want %q", data, original)
|
|
}
|
|
}
|
|
|
|
func TestBackupUnmanagedServiceFileSkipsManagedFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "kandev.service")
|
|
if err := os.WriteFile(path, []byte("# managed by kandev\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := backupUnmanagedServiceFile(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(path + ".bak"); !os.IsNotExist(err) {
|
|
t.Fatalf("managed file should not create backup, stat err=%v", err)
|
|
}
|
|
}
|