From 2f00f28b329110d10004c5152243707308cab95c Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 4 May 2026 08:46:40 +0300 Subject: task l: rename binary from mediaplayer to player --- cmd/mediaplayer/main.go | 205 ------------------------------------------- cmd/mediaplayer/main_test.go | 187 --------------------------------------- cmd/player/main.go | 205 +++++++++++++++++++++++++++++++++++++++++++ cmd/player/main_test.go | 187 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 392 insertions(+), 392 deletions(-) delete mode 100644 cmd/mediaplayer/main.go delete mode 100644 cmd/mediaplayer/main_test.go create mode 100644 cmd/player/main.go create mode 100644 cmd/player/main_test.go (limited to 'cmd') diff --git a/cmd/mediaplayer/main.go b/cmd/mediaplayer/main.go deleted file mode 100644 index d0143e2..0000000 --- a/cmd/mediaplayer/main.go +++ /dev/null @@ -1,205 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "log/slog" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "codeberg.org/snonux/player/internal" - "codeberg.org/snonux/player/internal/api" - "codeberg.org/snonux/player/internal/auth" - "codeberg.org/snonux/player/internal/clock" - "codeberg.org/snonux/player/internal/probe" - "codeberg.org/snonux/player/internal/repository" - "codeberg.org/snonux/player/internal/scanner" - "codeberg.org/snonux/player/internal/service" - "codeberg.org/snonux/player/internal/thumb" -) - -func main() { - if err := run(os.Args[1:]); err != nil { - slog.Error("fatal", "err", err) - os.Exit(1) - } -} - -func run(args []string) error { - return runWithSignal(args, nil) -} - -// appDeps bundles all wired service-layer dependencies. -type appDeps struct { - store repository.Store - hasher auth.Hasher - sm *auth.SessionManager - cfg *internal.Config - clk clock.Clock - mediaSvc service.MediaService - adminSvc service.AdminService - progressSvc service.ProgressService - authSvc service.AuthService - scanner scanner.Scanner - gcWorker *service.GCWorker - logger *slog.Logger -} - -// parseVersionFlag parses CLI flags and returns whether --version was requested. -func parseVersionFlag(args []string) (bool, error) { - fs := flag.NewFlagSet("mediaplayer", flag.ContinueOnError) - versionFlag := fs.Bool("version", false, "print version and exit") - if err := fs.Parse(args); err != nil { - return false, err - } - return *versionFlag, nil -} - -// buildLogger creates a slog.Logger aligned with the named log level. -func buildLogger(logLevel string) *slog.Logger { - var level slog.Level - switch logLevel { - case "debug": - level = slog.LevelDebug - case "info": - level = slog.LevelInfo - case "warn": - level = slog.LevelWarn - case "error": - level = slog.LevelError - default: - level = slog.LevelInfo - } - return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})) -} - -// wireDeps constructs the core service layer dependencies. -func wireDeps(cfg *internal.Config, store repository.Store, logger *slog.Logger, appCtx context.Context) *appDeps { - clk := clock.RealClock{} - hasher := auth.NewBCryptHasher(12) - sm := auth.NewSessionManager(store, clk, time.Duration(cfg.SessionTimeoutHours)*time.Hour) - - prober := probe.NewFFProber() - thumbGen := thumb.NewFFmpegGenerator() - mediaSvc := service.NewMediaService(store, clk, cfg.MediaRoot, thumbGen, prober) - - fsScanner := scanner.NewFSScannerWithLogger(store, prober, thumbGen, clk, cfg.MediaRoot, logger) - adminSvc := service.NewAdminServiceWithLogger(store, clk, hasher, fsScanner, cfg.MediaRoot, appCtx, logger) - - progressSvc := service.NewProgressService(store, clk) - authSvc := service.NewAuthService(store, clk, hasher, sm) - - gcWorker := service.NewGCWorker(store, clk, cfg.MediaRoot, time.Duration(cfg.GCIntervalMinutes)*time.Minute, logger) - gcWorker.Start() - - return &appDeps{ - store: store, - hasher: hasher, - sm: sm, - cfg: cfg, - clk: clk, - mediaSvc: mediaSvc, - adminSvc: adminSvc, - progressSvc: progressSvc, - authSvc: authSvc, - scanner: fsScanner, - gcWorker: gcWorker, - logger: logger, - } -} - -// ensureSignalChannel returns the provided channel or creates a new one wired -// to OS interrupt signals. -func ensureSignalChannel(sigCh <-chan os.Signal) <-chan os.Signal { - if sigCh != nil { - return sigCh - } - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - return quit -} - -// shutdownGracefully performs a timed graceful shutdown of the server. -func shutdownGracefully(gs *api.GracefulServer, logger *slog.Logger) error { - logger.Info("shutting down server") - shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := gs.Server.Shutdown(shutdownCtx); err != nil { - return fmt.Errorf("failed to shutdown server: %w", err) - } - logger.Info("server stopped") - return nil -} - -// runServer starts the HTTP server and blocks until shutdown. -func runServer(handler http.Handler, cfg *internal.Config, logger *slog.Logger, sigCh <-chan os.Signal) error { - gs := api.NewGracefulServer(handler, cfg) - - logger.Info("player starting", "version", internal.Version, "addr", gs.Server.Addr) - - errCh := make(chan error, 1) - go func() { - if err := gs.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - errCh <- fmt.Errorf("failed to start server: %w", err) - } - }() - - sigCh = ensureSignalChannel(sigCh) - - select { - case <-sigCh: - case err := <-errCh: - if err != nil { - return err - } - } - - return shutdownGracefully(gs, logger) -} - -func runWithSignal(args []string, sigCh <-chan os.Signal) error { - showVersion, err := parseVersionFlag(args) - if err != nil { - return err - } - if showVersion { - fmt.Println(internal.Version) - return nil - } - - cfg, err := internal.LoadConfig() - if err != nil { - return fmt.Errorf("failed to load config: %w", err) - } - - logger := buildLogger(cfg.LogLevel) - - store, err := repository.Open(cfg.DBPath) - if err != nil { - return fmt.Errorf("failed to open database: %w", err) - } - defer func() { - if err := store.Close(); err != nil { - logger.Error("failed to close database", "err", err) - } - }() - - appCtx, appCancel := context.WithCancel(context.Background()) - defer appCancel() - - deps := wireDeps(cfg, store, logger, appCtx) - defer deps.gcWorker.Stop() - - staticFS := http.Dir("web") - remuxer := probe.NewFFRemuxer() - server := api.NewServerWithLogger(store, deps.hasher, deps.sm, cfg, - deps.mediaSvc, deps.mediaSvc, deps.mediaSvc, deps.mediaSvc, deps.mediaSvc, deps.mediaSvc, - deps.adminSvc, deps.progressSvc, deps.authSvc, staticFS, remuxer, logger, - ) - - return runServer(server, cfg, logger, sigCh) -} diff --git a/cmd/mediaplayer/main_test.go b/cmd/mediaplayer/main_test.go deleted file mode 100644 index 29d36f0..0000000 --- a/cmd/mediaplayer/main_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package main - -import ( - "bytes" - "io" - "log/slog" - "os" - "path/filepath" - "strings" - "syscall" - "testing" - "time" - - "codeberg.org/snonux/player/internal" - "codeberg.org/snonux/player/internal/clock" - "codeberg.org/snonux/player/internal/repository" - "codeberg.org/snonux/player/internal/service" -) - -func captureStdout(fn func()) string { - old := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - fn() - w.Close() - os.Stdout = old - var buf bytes.Buffer - io.Copy(&buf, r) - return strings.TrimSpace(buf.String()) -} - -func TestGCWorkerWiring(t *testing.T) { - tmpDir := t.TempDir() - dbPath := filepath.Join(tmpDir, "test.db") - mediaRoot := filepath.Join(tmpDir, "media") - if err := os.MkdirAll(mediaRoot, 0o755); err != nil { - t.Fatalf("mkdir media root: %v", err) - } - - store, err := repository.Open(dbPath) - if err != nil { - t.Fatalf("open db: %v", err) - } - defer func() { - if err := store.Close(); err != nil { - t.Logf("close db: %v", err) - } - }() - - cfg := &internal.Config{ - GCIntervalMinutes: 1, - } - - logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) - clk := clock.RealClock{} - - w := service.NewGCWorker(store, clk, mediaRoot, time.Duration(cfg.GCIntervalMinutes)*time.Minute, logger) - w.Start() - w.Stop() // must not panic even after interacting with real store -} - -func TestRun_VersionFlag(t *testing.T) { - out := captureStdout(func() { - err := run([]string{"-version"}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - }) - if out != internal.Version { - t.Fatalf("expected %q, got %q", internal.Version, out) - } -} - -func TestRun_InvalidFlag(t *testing.T) { - err := run([]string{"-invalidflag"}) - if err == nil { - t.Fatal("expected error for invalid flag") - } -} - -func TestRun_InvalidConfig(t *testing.T) { - t.Setenv("PORT", "invalid") - err := run([]string{}) - if err == nil { - t.Fatal("expected error for invalid PORT") - } -} - -func TestRunWithSignal_NormalShutdown(t *testing.T) { - if os.Getenv("GO_TEST_IN_CONTAINER") == "no_ffprobe" { - t.Skip("ffprobe not available in this environment") - } - - tmpDir := t.TempDir() - t.Setenv("DB_PATH", filepath.Join(tmpDir, "test.db")) - t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media")) - t.Setenv("PORT", "0") - - // Build a channel we can use instead of real OS signals. - sigCh := make(chan os.Signal, 1) - - // Run the server in a goroutine; it will block on <-sigCh. - errCh := make(chan error, 1) - go func() { - errCh <- runWithSignal([]string{}, sigCh) - }() - - // Give the server a moment to start listening. - time.Sleep(500 * time.Millisecond) - - // Send a synthetic signal to trigger shutdown. - sigCh <- syscall.SIGINT - - // Wait for graceful shutdown. - select { - case err := <-errCh: - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - case <-time.After(10 * time.Second): - t.Fatal("timeout waiting for server shutdown") - } -} - -func TestRunWithSignal_LogLevels(t *testing.T) { - for _, level := range []string{"debug", "info", "warn", "error", "invalid"} { - t.Run(level, func(t *testing.T) { - tmpDir := t.TempDir() - t.Setenv("DB_PATH", filepath.Join(tmpDir, "test.db")) - t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media")) - t.Setenv("PORT", "0") - t.Setenv("LOG_LEVEL", level) - - sigCh := make(chan os.Signal, 1) - errCh := make(chan error, 1) - go func() { - errCh <- runWithSignal([]string{}, sigCh) - }() - time.Sleep(200 * time.Millisecond) - sigCh <- syscall.SIGTERM - - select { - case err := <-errCh: - if level == "invalid" { - if err == nil { - t.Fatal("expected error for invalid LOG_LEVEL") - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - case <-time.After(10 * time.Second): - t.Fatal("timeout waiting for server shutdown") - } - }) - } -} - -func TestRunWithSignal_InvalidDB(t *testing.T) { - tmpDir := t.TempDir() - t.Setenv("DB_PATH", filepath.Join(tmpDir, "readonly")) - if err := os.MkdirAll(filepath.Join(tmpDir, "readonly"), 0o755); err != nil { - t.Fatal(err) - } - t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media")) - t.Setenv("PORT", "0") - - err := runWithSignal([]string{}, nil) - if err == nil { - t.Fatal("expected error for invalid DB_PATH") - } -} - -func TestRunWithSignal_ServerErrorPath(t *testing.T) { - tmpDir := t.TempDir() - t.Setenv("DB_PATH", filepath.Join(tmpDir, "test.db")) - t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media")) - // Port 1 is privileged and should fail on non-root Linux. - t.Setenv("PORT", "1") - - // No signal channel; we expect the server start to fail quickly. - err := runWithSignal([]string{}, nil) - if err == nil { - t.Fatal("expected error when server cannot bind privileged port") - } -} diff --git a/cmd/player/main.go b/cmd/player/main.go new file mode 100644 index 0000000..2bb3336 --- /dev/null +++ b/cmd/player/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "codeberg.org/snonux/player/internal" + "codeberg.org/snonux/player/internal/api" + "codeberg.org/snonux/player/internal/auth" + "codeberg.org/snonux/player/internal/clock" + "codeberg.org/snonux/player/internal/probe" + "codeberg.org/snonux/player/internal/repository" + "codeberg.org/snonux/player/internal/scanner" + "codeberg.org/snonux/player/internal/service" + "codeberg.org/snonux/player/internal/thumb" +) + +func main() { + if err := run(os.Args[1:]); err != nil { + slog.Error("fatal", "err", err) + os.Exit(1) + } +} + +func run(args []string) error { + return runWithSignal(args, nil) +} + +// appDeps bundles all wired service-layer dependencies. +type appDeps struct { + store repository.Store + hasher auth.Hasher + sm *auth.SessionManager + cfg *internal.Config + clk clock.Clock + mediaSvc service.MediaService + adminSvc service.AdminService + progressSvc service.ProgressService + authSvc service.AuthService + scanner scanner.Scanner + gcWorker *service.GCWorker + logger *slog.Logger +} + +// parseVersionFlag parses CLI flags and returns whether --version was requested. +func parseVersionFlag(args []string) (bool, error) { + fs := flag.NewFlagSet("player", flag.ContinueOnError) + versionFlag := fs.Bool("version", false, "print version and exit") + if err := fs.Parse(args); err != nil { + return false, err + } + return *versionFlag, nil +} + +// buildLogger creates a slog.Logger aligned with the named log level. +func buildLogger(logLevel string) *slog.Logger { + var level slog.Level + switch logLevel { + case "debug": + level = slog.LevelDebug + case "info": + level = slog.LevelInfo + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + default: + level = slog.LevelInfo + } + return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})) +} + +// wireDeps constructs the core service layer dependencies. +func wireDeps(cfg *internal.Config, store repository.Store, logger *slog.Logger, appCtx context.Context) *appDeps { + clk := clock.RealClock{} + hasher := auth.NewBCryptHasher(12) + sm := auth.NewSessionManager(store, clk, time.Duration(cfg.SessionTimeoutHours)*time.Hour) + + prober := probe.NewFFProber() + thumbGen := thumb.NewFFmpegGenerator() + mediaSvc := service.NewMediaService(store, clk, cfg.MediaRoot, thumbGen, prober) + + fsScanner := scanner.NewFSScannerWithLogger(store, prober, thumbGen, clk, cfg.MediaRoot, logger) + adminSvc := service.NewAdminServiceWithLogger(store, clk, hasher, fsScanner, cfg.MediaRoot, appCtx, logger) + + progressSvc := service.NewProgressService(store, clk) + authSvc := service.NewAuthService(store, clk, hasher, sm) + + gcWorker := service.NewGCWorker(store, clk, cfg.MediaRoot, time.Duration(cfg.GCIntervalMinutes)*time.Minute, logger) + gcWorker.Start() + + return &appDeps{ + store: store, + hasher: hasher, + sm: sm, + cfg: cfg, + clk: clk, + mediaSvc: mediaSvc, + adminSvc: adminSvc, + progressSvc: progressSvc, + authSvc: authSvc, + scanner: fsScanner, + gcWorker: gcWorker, + logger: logger, + } +} + +// ensureSignalChannel returns the provided channel or creates a new one wired +// to OS interrupt signals. +func ensureSignalChannel(sigCh <-chan os.Signal) <-chan os.Signal { + if sigCh != nil { + return sigCh + } + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + return quit +} + +// shutdownGracefully performs a timed graceful shutdown of the server. +func shutdownGracefully(gs *api.GracefulServer, logger *slog.Logger) error { + logger.Info("shutting down server") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := gs.Server.Shutdown(shutdownCtx); err != nil { + return fmt.Errorf("failed to shutdown server: %w", err) + } + logger.Info("server stopped") + return nil +} + +// runServer starts the HTTP server and blocks until shutdown. +func runServer(handler http.Handler, cfg *internal.Config, logger *slog.Logger, sigCh <-chan os.Signal) error { + gs := api.NewGracefulServer(handler, cfg) + + logger.Info("player starting", "version", internal.Version, "addr", gs.Server.Addr) + + errCh := make(chan error, 1) + go func() { + if err := gs.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + errCh <- fmt.Errorf("failed to start server: %w", err) + } + }() + + sigCh = ensureSignalChannel(sigCh) + + select { + case <-sigCh: + case err := <-errCh: + if err != nil { + return err + } + } + + return shutdownGracefully(gs, logger) +} + +func runWithSignal(args []string, sigCh <-chan os.Signal) error { + showVersion, err := parseVersionFlag(args) + if err != nil { + return err + } + if showVersion { + fmt.Println(internal.Version) + return nil + } + + cfg, err := internal.LoadConfig() + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + logger := buildLogger(cfg.LogLevel) + + store, err := repository.Open(cfg.DBPath) + if err != nil { + return fmt.Errorf("failed to open database: %w", err) + } + defer func() { + if err := store.Close(); err != nil { + logger.Error("failed to close database", "err", err) + } + }() + + appCtx, appCancel := context.WithCancel(context.Background()) + defer appCancel() + + deps := wireDeps(cfg, store, logger, appCtx) + defer deps.gcWorker.Stop() + + staticFS := http.Dir("web") + remuxer := probe.NewFFRemuxer() + server := api.NewServerWithLogger(store, deps.hasher, deps.sm, cfg, + deps.mediaSvc, deps.mediaSvc, deps.mediaSvc, deps.mediaSvc, deps.mediaSvc, deps.mediaSvc, + deps.adminSvc, deps.progressSvc, deps.authSvc, staticFS, remuxer, logger, + ) + + return runServer(server, cfg, logger, sigCh) +} diff --git a/cmd/player/main_test.go b/cmd/player/main_test.go new file mode 100644 index 0000000..29d36f0 --- /dev/null +++ b/cmd/player/main_test.go @@ -0,0 +1,187 @@ +package main + +import ( + "bytes" + "io" + "log/slog" + "os" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + + "codeberg.org/snonux/player/internal" + "codeberg.org/snonux/player/internal/clock" + "codeberg.org/snonux/player/internal/repository" + "codeberg.org/snonux/player/internal/service" +) + +func captureStdout(fn func()) string { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + fn() + w.Close() + os.Stdout = old + var buf bytes.Buffer + io.Copy(&buf, r) + return strings.TrimSpace(buf.String()) +} + +func TestGCWorkerWiring(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "test.db") + mediaRoot := filepath.Join(tmpDir, "media") + if err := os.MkdirAll(mediaRoot, 0o755); err != nil { + t.Fatalf("mkdir media root: %v", err) + } + + store, err := repository.Open(dbPath) + if err != nil { + t.Fatalf("open db: %v", err) + } + defer func() { + if err := store.Close(); err != nil { + t.Logf("close db: %v", err) + } + }() + + cfg := &internal.Config{ + GCIntervalMinutes: 1, + } + + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) + clk := clock.RealClock{} + + w := service.NewGCWorker(store, clk, mediaRoot, time.Duration(cfg.GCIntervalMinutes)*time.Minute, logger) + w.Start() + w.Stop() // must not panic even after interacting with real store +} + +func TestRun_VersionFlag(t *testing.T) { + out := captureStdout(func() { + err := run([]string{"-version"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + if out != internal.Version { + t.Fatalf("expected %q, got %q", internal.Version, out) + } +} + +func TestRun_InvalidFlag(t *testing.T) { + err := run([]string{"-invalidflag"}) + if err == nil { + t.Fatal("expected error for invalid flag") + } +} + +func TestRun_InvalidConfig(t *testing.T) { + t.Setenv("PORT", "invalid") + err := run([]string{}) + if err == nil { + t.Fatal("expected error for invalid PORT") + } +} + +func TestRunWithSignal_NormalShutdown(t *testing.T) { + if os.Getenv("GO_TEST_IN_CONTAINER") == "no_ffprobe" { + t.Skip("ffprobe not available in this environment") + } + + tmpDir := t.TempDir() + t.Setenv("DB_PATH", filepath.Join(tmpDir, "test.db")) + t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media")) + t.Setenv("PORT", "0") + + // Build a channel we can use instead of real OS signals. + sigCh := make(chan os.Signal, 1) + + // Run the server in a goroutine; it will block on <-sigCh. + errCh := make(chan error, 1) + go func() { + errCh <- runWithSignal([]string{}, sigCh) + }() + + // Give the server a moment to start listening. + time.Sleep(500 * time.Millisecond) + + // Send a synthetic signal to trigger shutdown. + sigCh <- syscall.SIGINT + + // Wait for graceful shutdown. + select { + case err := <-errCh: + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + case <-time.After(10 * time.Second): + t.Fatal("timeout waiting for server shutdown") + } +} + +func TestRunWithSignal_LogLevels(t *testing.T) { + for _, level := range []string{"debug", "info", "warn", "error", "invalid"} { + t.Run(level, func(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("DB_PATH", filepath.Join(tmpDir, "test.db")) + t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media")) + t.Setenv("PORT", "0") + t.Setenv("LOG_LEVEL", level) + + sigCh := make(chan os.Signal, 1) + errCh := make(chan error, 1) + go func() { + errCh <- runWithSignal([]string{}, sigCh) + }() + time.Sleep(200 * time.Millisecond) + sigCh <- syscall.SIGTERM + + select { + case err := <-errCh: + if level == "invalid" { + if err == nil { + t.Fatal("expected error for invalid LOG_LEVEL") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + case <-time.After(10 * time.Second): + t.Fatal("timeout waiting for server shutdown") + } + }) + } +} + +func TestRunWithSignal_InvalidDB(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("DB_PATH", filepath.Join(tmpDir, "readonly")) + if err := os.MkdirAll(filepath.Join(tmpDir, "readonly"), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media")) + t.Setenv("PORT", "0") + + err := runWithSignal([]string{}, nil) + if err == nil { + t.Fatal("expected error for invalid DB_PATH") + } +} + +func TestRunWithSignal_ServerErrorPath(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("DB_PATH", filepath.Join(tmpDir, "test.db")) + t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media")) + // Port 1 is privileged and should fail on non-root Linux. + t.Setenv("PORT", "1") + + // No signal channel; we expect the server start to fail quickly. + err := runWithSignal([]string{}, nil) + if err == nil { + t.Fatal("expected error when server cannot bind privileged port") + } +} -- cgit v1.2.3