package daemon import ( "context" "encoding/json" "errors" "io" "log/slog" "net" "os" "path/filepath" "strings" "syscall" "testing" "time" "banger/internal/api" "banger/internal/buildinfo" "banger/internal/model" "banger/internal/paths" "banger/internal/rpc" "banger/internal/system" ) // TestAuthorizeConnRejectsNonUnixConn pins the type guard at the top // of authorizeConn: SO_PEERCRED only makes sense on a unix socket, so // anything else must be refused outright. net.Pipe gives us a // connection that satisfies net.Conn but isn't a *net.UnixConn, which // is exactly the shape we need to exercise the early-return. func TestAuthorizeConnRejectsNonUnixConn(t *testing.T) { d := &Daemon{} pipeA, pipeB := net.Pipe() defer pipeA.Close() defer pipeB.Close() if err := d.authorizeConn(pipeA); err == nil { t.Fatal("authorizeConn(pipe) succeeded, want error") } } // TestAuthorizeConnAcceptsOwnerUIDOverUnixSocket pins the happy path: // when the test process connects to a freshly bound unix socket as // itself, the daemon's peer-cred check matches d.clientUID and lets // the connection through. func TestAuthorizeConnAcceptsOwnerUIDOverUnixSocket(t *testing.T) { dir := t.TempDir() sockPath := filepath.Join(dir, "test.sock") listener, err := net.Listen("unix", sockPath) if err != nil { t.Fatalf("listen: %v", err) } defer listener.Close() type result struct { err error } got := make(chan result, 1) go func() { conn, err := listener.Accept() if err != nil { got <- result{err: err} return } defer conn.Close() d := &Daemon{clientUID: os.Getuid()} got <- result{err: d.authorizeConn(conn)} }() client, err := net.Dial("unix", sockPath) if err != nil { t.Fatalf("dial: %v", err) } defer client.Close() select { case r := <-got: if r.err != nil { t.Fatalf("authorizeConn(unix self) = %v, want nil", r.err) } case <-time.After(2 * time.Second): t.Fatal("authorizeConn never returned") } } func TestRegisterImageRequiresKernel(t *testing.T) { rootfs := filepath.Join(t.TempDir(), "rootfs.ext4") if err := os.WriteFile(rootfs, []byte("rootfs"), 0o644); err != nil { t.Fatalf("write rootfs: %v", err) } d := &Daemon{store: openDaemonStore(t)} wireServices(d) _, err := d.img.RegisterImage(context.Background(), api.ImageRegisterParams{ Name: "missing-kernel", RootfsPath: rootfs, }) if err == nil || !strings.Contains(err.Error(), "kernel path is required") { t.Fatalf("RegisterImage() error = %v", err) } } func TestDispatchPingIncludesBuildInfo(t *testing.T) { d := &Daemon{pid: 42} wireServices(d) resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version, Method: "ping"}) if !resp.OK { t.Fatalf("dispatch(ping) = %+v, want ok", resp) } var got api.PingResult if err := json.Unmarshal(resp.Result, &got); err != nil { t.Fatalf("Unmarshal(PingResult): %v", err) } info := buildinfo.Current() if got.Status != "ok" || got.PID != 42 { t.Fatalf("PingResult = %+v, want status/pid populated", got) } if got.Version != info.Version || got.Commit != info.Commit || got.BuiltAt != info.BuiltAt { t.Fatalf("PingResult build info = %+v, want %+v", got, info) } } func TestServeReturnsOnContextCancel(t *testing.T) { dir := t.TempDir() runtimeDir := filepath.Join(dir, "runtime") if err := os.MkdirAll(runtimeDir, 0o755); err != nil { t.Fatalf("MkdirAll runtime: %v", err) } socketPath := filepath.Join(runtimeDir, "bangerd.sock") probe, err := net.Listen("unix", filepath.Join(runtimeDir, "probe.sock")) if err != nil { if errors.Is(err, syscall.EPERM) || strings.Contains(err.Error(), "operation not permitted") { t.Skipf("unix socket listen blocked in this environment: %v", err) } t.Fatalf("probe listen: %v", err) } _ = probe.Close() _ = os.Remove(filepath.Join(runtimeDir, "probe.sock")) d := &Daemon{ layout: paths.Layout{ RuntimeDir: runtimeDir, SocketPath: socketPath, }, config: model.DaemonConfig{ StatsPollInterval: time.Hour, }, store: openDaemonStore(t), runner: system.NewRunner(), logger: slog.New(slog.NewTextHandler(io.Discard, nil)), closing: make(chan struct{}), clientUID: -1, clientGID: -1, } wireServices(d) ctx, cancel := context.WithCancel(context.Background()) defer cancel() serveErr := make(chan error, 1) go func() { serveErr <- d.Serve(ctx) }() deadline := time.Now().Add(2 * time.Second) for { if _, err := os.Stat(socketPath); err == nil { break } select { case err := <-serveErr: t.Fatalf("Serve() returned before socket was ready: %v", err) default: } if time.Now().After(deadline) { t.Fatalf("socket %s not created before deadline", socketPath) } time.Sleep(25 * time.Millisecond) } cancel() select { case err := <-serveErr: if err != nil { t.Fatalf("Serve() error = %v, want nil on context cancel", err) } case <-time.After(2 * time.Second): t.Fatal("Serve() did not return after context cancel") } } func TestPromoteImageCopiesBootArtifactsIntoArtifactDir(t *testing.T) { dir := t.TempDir() rootfs := filepath.Join(dir, "rootfs.ext4") kernel := filepath.Join(dir, "vmlinux") initrd := filepath.Join(dir, "initrd.img") modulesDir := filepath.Join(dir, "modules") if err := os.MkdirAll(modulesDir, 0o755); err != nil { t.Fatalf("mkdir modules: %v", err) } for path, data := range map[string]string{ rootfs: "rootfs", kernel: "kernel", initrd: "initrd", filepath.Join(modulesDir, "depmod"): "modules", } { if err := os.WriteFile(path, []byte(data), 0o644); err != nil { t.Fatalf("write %s: %v", path, err) } } db := openDaemonStore(t) image := model.Image{ ID: "img-promote", Name: "void", Managed: false, RootfsPath: rootfs, KernelPath: kernel, InitrdPath: initrd, ModulesDir: modulesDir, CreatedAt: model.Now(), UpdatedAt: model.Now(), } if err := db.UpsertImage(context.Background(), image); err != nil { t.Fatalf("UpsertImage: %v", err) } imagesDir := filepath.Join(dir, "images") if err := os.MkdirAll(imagesDir, 0o755); err != nil { t.Fatalf("mkdir images dir: %v", err) } d := &Daemon{ layout: paths.Layout{ImagesDir: imagesDir}, store: db, runner: system.NewRunner(), } wireServices(d) got, err := d.img.PromoteImage(context.Background(), image.Name) if err != nil { t.Fatalf("PromoteImage: %v", err) } if !got.Managed { t.Fatal("promoted image should be managed") } for _, path := range []string{got.RootfsPath, got.KernelPath, got.InitrdPath, got.ModulesDir} { if !strings.HasPrefix(path, got.ArtifactDir) { t.Fatalf("artifact path %q does not live under %q", path, got.ArtifactDir) } if _, err := os.Stat(path); err != nil { t.Fatalf("stat %s: %v", path, err) } } }