@@ -2,6 +2,8 @@ package server
22
33import (
44 "fmt"
5+ "io"
6+ "log/slog"
57 "os"
68 "strings"
79 "testing"
@@ -477,6 +479,218 @@ func TestServerCmd_AllowedHosts(t *testing.T) {
477479 }
478480}
479481
482+ func TestServerCmd_StatePersistenceFlags (t * testing.T ) {
483+ // NOTE: These tests use --exit flag to test flag parsing and defaults.
484+ // Runtime validation that happens in runServer (e.g., "--load-state requires --state-file")
485+ // would call os.Exit(1) which terminates the test process, so those validations
486+ // are tested through integration/E2E tests instead.
487+
488+ t .Run ("state-file with defaults" , func (t * testing.T ) {
489+ isolateViper (t )
490+
491+ serverCmd := CreateServerCmd ()
492+ setupCommandOutput (t , serverCmd )
493+ serverCmd .SetArgs ([]string {"--state-file" , "/tmp/state.json" , "--exit" , "dummy-command" })
494+ err := serverCmd .Execute ()
495+ require .NoError (t , err )
496+
497+ assert .Equal (t , "/tmp/state.json" , viper .GetString (StateFile ))
498+ // load-state and save-state default to true when state-file is set (validated in runServer)
499+ })
500+
501+ t .Run ("state-file with explicit load-state=false" , func (t * testing.T ) {
502+ isolateViper (t )
503+
504+ serverCmd := CreateServerCmd ()
505+ setupCommandOutput (t , serverCmd )
506+ serverCmd .SetArgs ([]string {"--state-file" , "/tmp/state.json" , "--load-state=false" , "--exit" , "dummy-command" })
507+ err := serverCmd .Execute ()
508+ require .NoError (t , err )
509+
510+ assert .Equal (t , "/tmp/state.json" , viper .GetString (StateFile ))
511+ assert .Equal (t , false , viper .GetBool (LoadState ))
512+ })
513+
514+ t .Run ("state-file with explicit save-state=false" , func (t * testing.T ) {
515+ isolateViper (t )
516+
517+ serverCmd := CreateServerCmd ()
518+ setupCommandOutput (t , serverCmd )
519+ serverCmd .SetArgs ([]string {"--state-file" , "/tmp/state.json" , "--save-state=false" , "--exit" , "dummy-command" })
520+ err := serverCmd .Execute ()
521+ require .NoError (t , err )
522+
523+ assert .Equal (t , "/tmp/state.json" , viper .GetString (StateFile ))
524+ assert .Equal (t , false , viper .GetBool (SaveState ))
525+ })
526+
527+ t .Run ("state-file with explicit load-state=true and save-state=true" , func (t * testing.T ) {
528+ isolateViper (t )
529+
530+ serverCmd := CreateServerCmd ()
531+ setupCommandOutput (t , serverCmd )
532+ serverCmd .SetArgs ([]string {
533+ "--state-file" , "/tmp/state.json" ,
534+ "--load-state=true" ,
535+ "--save-state=true" ,
536+ "--exit" , "dummy-command" ,
537+ })
538+ err := serverCmd .Execute ()
539+ require .NoError (t , err )
540+
541+ assert .Equal (t , "/tmp/state.json" , viper .GetString (StateFile ))
542+ assert .Equal (t , true , viper .GetBool (LoadState ))
543+ assert .Equal (t , true , viper .GetBool (SaveState ))
544+ })
545+
546+ t .Run ("load-state flag can be parsed" , func (t * testing.T ) {
547+ isolateViper (t )
548+
549+ serverCmd := CreateServerCmd ()
550+ setupCommandOutput (t , serverCmd )
551+ serverCmd .SetArgs ([]string {"--load-state" , "--exit" , "dummy-command" })
552+ err := serverCmd .Execute ()
553+ require .NoError (t , err )
554+
555+ // Flag is parsed correctly (validation happens in runServer)
556+ assert .Equal (t , true , viper .GetBool (LoadState ))
557+ })
558+
559+ t .Run ("save-state flag can be parsed" , func (t * testing.T ) {
560+ isolateViper (t )
561+
562+ serverCmd := CreateServerCmd ()
563+ setupCommandOutput (t , serverCmd )
564+ serverCmd .SetArgs ([]string {"--save-state" , "--exit" , "dummy-command" })
565+ err := serverCmd .Execute ()
566+ require .NoError (t , err )
567+
568+ // Flag is parsed correctly (validation happens in runServer)
569+ assert .Equal (t , true , viper .GetBool (SaveState ))
570+ })
571+
572+ t .Run ("pid-file can be set independently" , func (t * testing.T ) {
573+ isolateViper (t )
574+
575+ serverCmd := CreateServerCmd ()
576+ setupCommandOutput (t , serverCmd )
577+ serverCmd .SetArgs ([]string {"--pid-file" , "/tmp/server.pid" , "--exit" , "dummy-command" })
578+ err := serverCmd .Execute ()
579+ require .NoError (t , err )
580+
581+ assert .Equal (t , "/tmp/server.pid" , viper .GetString (PidFile ))
582+ })
583+
584+ t .Run ("state-file and pid-file can be set together" , func (t * testing.T ) {
585+ isolateViper (t )
586+
587+ serverCmd := CreateServerCmd ()
588+ setupCommandOutput (t , serverCmd )
589+ serverCmd .SetArgs ([]string {
590+ "--state-file" , "/tmp/state.json" ,
591+ "--pid-file" , "/tmp/server.pid" ,
592+ "--exit" , "dummy-command" ,
593+ })
594+ err := serverCmd .Execute ()
595+ require .NoError (t , err )
596+
597+ assert .Equal (t , "/tmp/state.json" , viper .GetString (StateFile ))
598+ assert .Equal (t , "/tmp/server.pid" , viper .GetString (PidFile ))
599+ })
600+ }
601+
602+ func TestPIDFileOperations (t * testing.T ) {
603+ discardLogger := slog .New (slog .NewTextHandler (io .Discard , nil ))
604+
605+ t .Run ("writePIDFile creates file with process ID" , func (t * testing.T ) {
606+ tmpDir := t .TempDir ()
607+ pidFile := tmpDir + "/test.pid"
608+
609+ err := writePIDFile (pidFile , discardLogger )
610+ require .NoError (t , err )
611+
612+ // Verify file exists
613+ _ , err = os .Stat (pidFile )
614+ require .NoError (t , err )
615+
616+ // Verify content contains current PID
617+ data , err := os .ReadFile (pidFile )
618+ require .NoError (t , err )
619+
620+ expectedPID := fmt .Sprintf ("%d\n " , os .Getpid ())
621+ assert .Equal (t , expectedPID , string (data ))
622+ })
623+
624+ t .Run ("writePIDFile creates directory if not exists" , func (t * testing.T ) {
625+ tmpDir := t .TempDir ()
626+ pidFile := tmpDir + "/nested/deep/test.pid"
627+
628+ err := writePIDFile (pidFile , discardLogger )
629+ require .NoError (t , err )
630+
631+ // Verify file exists
632+ _ , err = os .Stat (pidFile )
633+ require .NoError (t , err )
634+
635+ // Verify directory was created
636+ _ , err = os .Stat (tmpDir + "/nested/deep" )
637+ require .NoError (t , err )
638+ })
639+
640+ t .Run ("writePIDFile overwrites existing file" , func (t * testing.T ) {
641+ tmpDir := t .TempDir ()
642+ pidFile := tmpDir + "/test.pid"
643+
644+ // Write initial PID file
645+ err := os .WriteFile (pidFile , []byte ("12345\n " ), 0o644 )
646+ require .NoError (t , err )
647+
648+ // Overwrite with current PID
649+ err = writePIDFile (pidFile , discardLogger )
650+ require .NoError (t , err )
651+
652+ // Verify content is updated
653+ data , err := os .ReadFile (pidFile )
654+ require .NoError (t , err )
655+
656+ expectedPID := fmt .Sprintf ("%d\n " , os .Getpid ())
657+ assert .Equal (t , expectedPID , string (data ))
658+ })
659+
660+ t .Run ("cleanupPIDFile removes file" , func (t * testing.T ) {
661+ tmpDir := t .TempDir ()
662+ pidFile := tmpDir + "/test.pid"
663+
664+ // Create PID file
665+ err := os .WriteFile (pidFile , []byte ("12345\n " ), 0o644 )
666+ require .NoError (t , err )
667+
668+ // Cleanup
669+ cleanupPIDFile (pidFile , discardLogger )
670+
671+ // Verify file is removed
672+ _ , err = os .Stat (pidFile )
673+ assert .True (t , os .IsNotExist (err ))
674+ })
675+
676+ t .Run ("cleanupPIDFile handles non-existent file" , func (t * testing.T ) {
677+ tmpDir := t .TempDir ()
678+ pidFile := tmpDir + "/nonexistent.pid"
679+
680+ // Should not panic or error
681+ cleanupPIDFile (pidFile , discardLogger )
682+ })
683+
684+ t .Run ("cleanupPIDFile handles directory removal error gracefully" , func (t * testing.T ) {
685+ // Create a file in a protected directory (this is system-dependent)
686+ // Just verify it doesn't panic when it can't remove the file
687+ pidFile := "/this/should/not/exist/test.pid"
688+
689+ // Should not panic
690+ cleanupPIDFile (pidFile , discardLogger )
691+ })
692+ }
693+
480694func TestServerCmd_AllowedOrigins (t * testing.T ) {
481695 tests := []struct {
482696 name string
0 commit comments