package cmd import ( "bytes" "context" "fmt" "os" "path/filepath" "syscall" "testing" "time" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestStartCommand(t *testing.T) { // Test start command structure assert.Equal(t, "start", startCmd.Use) assert.Contains(t, startCmd.Short, "Start") assert.Contains(t, startCmd.Long, "registry") } func TestRunStart_ConfigError(t *testing.T) { // Save original values originalCfgFile := cfgFile defer func() { cfgFile = originalCfgFile viper.Reset() }() // Set non-existent config file cfgFile = "/nonexistent/config.toml" // Capture stderr to check config error message var stderr bytes.Buffer originalStderr := os.Stderr r, w, _ := os.Pipe() os.Stderr = w // Initialize config to trigger error handling done := make(chan bool) go func() { defer func() { if recover() != nil { // Recovery from log.Fatal is expected in some cases } done <- true }() // Call initConfig directly to test config error handling initConfig() }() // Give it a moment to process select { case <-done: // Expected case <-time.After(1 * time.Second): t.Fatal("initConfig should have completed quickly") } w.Close() os.Stderr = originalStderr stderr.ReadFrom(r) // Check that an error message was printed stderrContent := stderr.String() assert.Contains(t, stderrContent, "Error reading config file") } func TestCreatePidFile(t *testing.T) { // Test PID file creation pidFile := createPidFile() if pidFile != "" { // Verify file exists and contains valid PID assert.FileExists(t, pidFile) content, err := os.ReadFile(pidFile) require.NoError(t, err) var pid int _, err = fmt.Sscanf(string(content), "%d", &pid) assert.NoError(t, err) assert.Equal(t, os.Getpid(), pid) // Clean up removePidFile(pidFile) assert.NoFileExists(t, pidFile) } // Test should pass even if no writable location is found // (function returns empty string in that case) } func TestRemovePidFile(t *testing.T) { tests := []struct { name string setupFunc func(t *testing.T) string expectError bool }{ { name: "remove existing file", setupFunc: func(t *testing.T) string { tempDir := t.TempDir() pidFile := filepath.Join(tempDir, "test.pid") err := os.WriteFile(pidFile, []byte("12345"), 0644) require.NoError(t, err) return pidFile }, expectError: false, }, { name: "remove non-existent file", setupFunc: func(t *testing.T) string { tempDir := t.TempDir() return filepath.Join(tempDir, "nonexistent.pid") }, expectError: true, // Warning logged, but no panic }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pidFile := tt.setupFunc(t) // Should not panic regardless of whether file exists assert.NotPanics(t, func() { removePidFile(pidFile) }) // File should not exist after removal attempt assert.NoFileExists(t, pidFile) }) } } func TestCreatePidFile_Locations(t *testing.T) { // Test that createPidFile tries multiple locations // We can't easily mock os.WriteFile to test all failure scenarios, // but we can verify the function doesn't panic assert.NotPanics(t, func() { pidFile := createPidFile() if pidFile != "" { removePidFile(pidFile) } }) } func TestCreatePidFile_Content(t *testing.T) { pidFile := createPidFile() if pidFile == "" { t.Skip("No writable location found for PID file") } defer removePidFile(pidFile) // Verify PID file contains current process ID content, err := os.ReadFile(pidFile) require.NoError(t, err) var storedPid int _, err = fmt.Sscanf(string(content), "%d", &storedPid) require.NoError(t, err) assert.Equal(t, os.Getpid(), storedPid) } func TestPidFilePermissions(t *testing.T) { pidFile := createPidFile() if pidFile == "" { t.Skip("No writable location found for PID file") } defer removePidFile(pidFile) // Check file permissions info, err := os.Stat(pidFile) require.NoError(t, err) // Should be readable by owner and group assert.True(t, info.Mode().Perm()&0644 != 0) } func TestPidFileLocations(t *testing.T) { // Test different potential PID file locations // This mainly tests that the function handles various scenarios originalHome := os.Getenv("HOME") defer os.Setenv("HOME", originalHome) // Test with invalid home directory os.Setenv("HOME", "/nonexistent/invalid/path") assert.NotPanics(t, func() { pidFile := createPidFile() if pidFile != "" { removePidFile(pidFile) } }) } func TestStartCmdStructure(t *testing.T) { // Verify command is properly registered found := false for _, cmd := range rootCmd.Commands() { if cmd.Name() == "start" { found = true break } } assert.True(t, found, "start command should be registered with root command") } func TestStartCmdHelp(t *testing.T) { var output bytes.Buffer // Create a copy of the command to avoid affecting the global command cmd := &cobra.Command{ Use: startCmd.Use, Short: startCmd.Short, Long: startCmd.Long, Run: startCmd.Run, } cmd.SetOut(&output) cmd.SetErr(&output) cmd.SetArgs([]string{"--help"}) err := cmd.Execute() assert.NoError(t, err) helpOutput := output.String() assert.Contains(t, helpOutput, "Start") assert.Contains(t, helpOutput, "container registry") assert.Contains(t, helpOutput, "reverse proxy") assert.Contains(t, helpOutput, "server") } // Test helper functions for signal handling scenarios func TestStartCommand_SignalHandling(t *testing.T) { // This is a simplified test since we can't easily test the full signal handling // without actually starting the server. We mainly test that the command structure is correct. // Verify the command can be created and has the right properties assert.NotNil(t, startCmd.Run) assert.Equal(t, "start", startCmd.Use) } func TestRunStart_ValidConfig(t *testing.T) { // Create a valid config file tempDir := t.TempDir() configFile := filepath.Join(tempDir, "gordon.toml") configContent := ` [server] port = 8080 registry_port = 5000 runtime = "docker" [logging] enabled = false dir = "` + filepath.Join(tempDir, "logs") + `" [registry] enabled = false [routes] ` err := os.WriteFile(configFile, []byte(configContent), 0644) require.NoError(t, err) // Save original values originalCfgFile := cfgFile defer func() { cfgFile = originalCfgFile viper.Reset() }() // Set config file cfgFile = configFile viper.Reset() // We can't actually run the full start command without Docker, // but we can test that it doesn't immediately fail with config errors // by mocking the execution // This is a limited test since runStart calls log.Fatal on various errors // and we can't easily test the full server startup without actual dependencies assert.NotNil(t, startCmd.Run, "Start command should have a run function") } func TestSignalHandling_Concepts(t *testing.T) { // Test the concepts used in signal handling // Test that we can create signal channels (used in runStart) sigChan := make(chan os.Signal, 1) reloadChan := make(chan os.Signal, 1) assert.NotNil(t, sigChan) assert.NotNil(t, reloadChan) // Test signal types that would be used signals := []os.Signal{ syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1, } for _, sig := range signals { assert.NotNil(t, sig) } } func TestContextCancellation(t *testing.T) { // Test context cancellation patterns used in runStart ctx, cancel := context.WithCancel(context.Background()) assert.NotNil(t, ctx) assert.NotNil(t, cancel) // Test timeout context timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 30*time.Second) assert.NotNil(t, timeoutCtx) assert.NotNil(t, timeoutCancel) // Clean up cancel() timeoutCancel() }