package cli import ( "context" "errors" "os" "path/filepath" "strings" "testing" "banger/internal/api" "banger/internal/installmeta" ) func TestEnsureDaemonRequiresSystemInstallWhenMetadataMissing(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "config")) t.Setenv("XDG_STATE_HOME", filepath.Join(t.TempDir(), "state")) t.Setenv("XDG_CACHE_HOME", filepath.Join(t.TempDir(), "cache")) t.Setenv("XDG_RUNTIME_DIR", filepath.Join(t.TempDir(), "run")) restoreLoad := loadInstallMetadata restoreUID := currentUID t.Cleanup(func() { loadInstallMetadata = restoreLoad currentUID = restoreUID }) loadInstallMetadata = func() (installmeta.Metadata, error) { return installmeta.Metadata{}, os.ErrNotExist } currentUID = os.Getuid d := defaultDeps() d.daemonPing = func(context.Context, string) (api.PingResult, error) { return api.PingResult{}, errors.New("dial unix /run/banger/bangerd.sock: no such file") } _, _, err := d.ensureDaemon(context.Background()) if err == nil || !strings.Contains(err.Error(), "sudo banger system install") { t.Fatalf("ensureDaemon error = %v, want install guidance", err) } } func TestEnsureDaemonSuggestsRestartWhenInstalledButUnavailable(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "config")) t.Setenv("XDG_STATE_HOME", filepath.Join(t.TempDir(), "state")) t.Setenv("XDG_CACHE_HOME", filepath.Join(t.TempDir(), "cache")) t.Setenv("XDG_RUNTIME_DIR", filepath.Join(t.TempDir(), "run")) restoreLoad := loadInstallMetadata restoreUID := currentUID t.Cleanup(func() { loadInstallMetadata = restoreLoad currentUID = restoreUID }) loadInstallMetadata = func() (installmeta.Metadata, error) { return installmeta.Metadata{ OwnerUser: "tester", OwnerUID: os.Getuid(), OwnerGID: os.Getgid(), OwnerHome: t.TempDir(), }, nil } currentUID = os.Getuid d := defaultDeps() d.daemonPing = func(context.Context, string) (api.PingResult, error) { return api.PingResult{}, errors.New("dial unix /run/banger/bangerd.sock: connection refused") } _, _, err := d.ensureDaemon(context.Background()) if err == nil || !strings.Contains(err.Error(), "sudo banger system restart") { t.Fatalf("ensureDaemon error = %v, want restart guidance", err) } } func TestEnsureDaemonRejectsNonOwnerUser(t *testing.T) { restoreLoad := loadInstallMetadata restoreUID := currentUID t.Cleanup(func() { loadInstallMetadata = restoreLoad currentUID = restoreUID }) loadInstallMetadata = func() (installmeta.Metadata, error) { return installmeta.Metadata{ OwnerUser: "alice", OwnerUID: os.Getuid() + 1, OwnerGID: os.Getgid(), OwnerHome: t.TempDir(), }, nil } currentUID = os.Getuid d := defaultDeps() d.daemonPing = func(context.Context, string) (api.PingResult, error) { t.Fatal("daemonPing should not be called for a non-owner user") return api.PingResult{}, nil } _, _, err := d.ensureDaemon(context.Background()) if err == nil || !strings.Contains(err.Error(), "installed for alice") { t.Fatalf("ensureDaemon error = %v, want owner mismatch guidance", err) } } func TestSystemSubcommandFlagsAreScoped(t *testing.T) { root := NewBangerCommand() systemCmd, _, err := root.Find([]string{"system"}) if err != nil { t.Fatalf("find system: %v", err) } installCmd, _, err := systemCmd.Find([]string{"install"}) if err != nil { t.Fatalf("find system install: %v", err) } uninstallCmd, _, err := systemCmd.Find([]string{"uninstall"}) if err != nil { t.Fatalf("find system uninstall: %v", err) } if installCmd.Flags().Lookup("owner") == nil { t.Fatal("system install is missing --owner") } if uninstallCmd.Flags().Lookup("purge") == nil { t.Fatal("system uninstall is missing --purge") } } func TestRenderSystemdUnitIncludesHardeningDirectives(t *testing.T) { unit := renderSystemdUnit(installmeta.Metadata{ OwnerUser: "alice", OwnerUID: 1000, OwnerGID: 1000, OwnerHome: "/home/alice/dev home", }) for _, want := range []string{ "ExecStart=/usr/local/bin/bangerd --system", "User=alice", "Wants=network-online.target bangerd-root.service", "After=bangerd-root.service", "Requires=bangerd-root.service", "UMask=0077", "Environment=TMPDIR=/run/banger", "NoNewPrivileges=yes", "PrivateMounts=yes", "ProtectSystem=strict", "ProtectHome=read-only", "ProtectControlGroups=yes", "ProtectKernelLogs=yes", "ProtectKernelModules=yes", "ProtectClock=yes", "ProtectHostname=yes", "RestrictSUIDSGID=yes", "LockPersonality=yes", "SystemCallArchitectures=native", "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_VSOCK", "StateDirectory=banger", "StateDirectoryMode=0700", "CacheDirectory=banger", "CacheDirectoryMode=0700", "RuntimeDirectory=banger", "RuntimeDirectoryMode=0700", `ReadOnlyPaths="/home/alice/dev home"`, } { if !strings.Contains(unit, want) { t.Fatalf("unit = %q, want %q", unit, want) } } } func TestRenderRootHelperSystemdUnitIncludesRequiredCapabilities(t *testing.T) { unit := renderRootHelperSystemdUnit() for _, want := range []string{ "ExecStart=/usr/local/bin/bangerd --root-helper", "Environment=TMPDIR=/run/banger-root", "NoNewPrivileges=yes", "PrivateTmp=yes", "PrivateMounts=yes", "ProtectSystem=strict", "ProtectHome=yes", "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_VSOCK", "CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER CAP_KILL CAP_MKNOD CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_ADMIN CAP_SYS_CHROOT", "ReadWritePaths=/var/lib/banger", "RuntimeDirectory=banger-root", "RuntimeDirectoryMode=0711", } { if !strings.Contains(unit, want) { t.Fatalf("unit = %q, want %q", unit, want) } } } func TestRenderSystemdUnitsIncludeOptionalCoverageEnv(t *testing.T) { t.Setenv(systemCoverDirEnv, "/var/lib/banger") t.Setenv(rootCoverDirEnv, "/var/lib/banger") userUnit := renderSystemdUnit(installmeta.Metadata{ OwnerUser: "alice", OwnerUID: 1000, OwnerGID: 1000, OwnerHome: "/home/alice", }) if !strings.Contains(userUnit, `Environment=GOCOVERDIR="/var/lib/banger"`) { t.Fatalf("user unit = %q, want GOCOVERDIR env", userUnit) } rootUnit := renderRootHelperSystemdUnit() if !strings.Contains(rootUnit, `Environment=GOCOVERDIR="/var/lib/banger"`) { t.Fatalf("root unit = %q, want GOCOVERDIR env", rootUnit) } }