1package cmd
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "os"
8 "path/filepath"
9 "syscall"
10 "testing"
11 "time"
12
13 "github.com/spf13/cobra"
14 "github.com/spf13/viper"
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17)
18
19func TestStartCommand(t *testing.T) {
20 // Test start command structure
21 assert.Equal(t, "start", startCmd.Use)
22 assert.Contains(t, startCmd.Short, "Start")
23 assert.Contains(t, startCmd.Long, "registry")
24}
25
26func TestRunStart_ConfigError(t *testing.T) {
27 // Save original values
28 originalCfgFile := cfgFile
29 defer func() {
30 cfgFile = originalCfgFile
31 viper.Reset()
32 }()
33
34 // Set non-existent config file
35 cfgFile = "/nonexistent/config.toml"
36
37 // Capture stderr to check config error message
38 var stderr bytes.Buffer
39 originalStderr := os.Stderr
40 r, w, _ := os.Pipe()
41 os.Stderr = w
42
43 // Initialize config to trigger error handling
44 done := make(chan bool)
45 go func() {
46 defer func() {
47 if recover() != nil {
48 // Recovery from log.Fatal is expected in some cases
49 }
50 done <- true
51 }()
52
53 // Call initConfig directly to test config error handling
54 initConfig()
55 }()
56
57 // Give it a moment to process
58 select {
59 case <-done:
60 // Expected
61 case <-time.After(1 * time.Second):
62 t.Fatal("initConfig should have completed quickly")
63 }
64
65 w.Close()
66 os.Stderr = originalStderr
67 stderr.ReadFrom(r)
68
69 // Check that an error message was printed
70 stderrContent := stderr.String()
71 assert.Contains(t, stderrContent, "Error reading config file")
72}
73
74func TestCreatePidFile(t *testing.T) {
75 // Test PID file creation
76 pidFile := createPidFile()
77
78 if pidFile != "" {
79 // Verify file exists and contains valid PID
80 assert.FileExists(t, pidFile)
81
82 content, err := os.ReadFile(pidFile)
83 require.NoError(t, err)
84
85 var pid int
86 _, err = fmt.Sscanf(string(content), "%d", &pid)
87 assert.NoError(t, err)
88 assert.Equal(t, os.Getpid(), pid)
89
90 // Clean up
91 removePidFile(pidFile)
92 assert.NoFileExists(t, pidFile)
93 }
94
95 // Test should pass even if no writable location is found
96 // (function returns empty string in that case)
97}
98
99func TestRemovePidFile(t *testing.T) {
100 tests := []struct {
101 name string
102 setupFunc func(t *testing.T) string
103 expectError bool
104 }{
105 {
106 name: "remove existing file",
107 setupFunc: func(t *testing.T) string {
108 tempDir := t.TempDir()
109 pidFile := filepath.Join(tempDir, "test.pid")
110 err := os.WriteFile(pidFile, []byte("12345"), 0644)
111 require.NoError(t, err)
112 return pidFile
113 },
114 expectError: false,
115 },
116 {
117 name: "remove non-existent file",
118 setupFunc: func(t *testing.T) string {
119 tempDir := t.TempDir()
120 return filepath.Join(tempDir, "nonexistent.pid")
121 },
122 expectError: true, // Warning logged, but no panic
123 },
124 }
125
126 for _, tt := range tests {
127 t.Run(tt.name, func(t *testing.T) {
128 pidFile := tt.setupFunc(t)
129
130 // Should not panic regardless of whether file exists
131 assert.NotPanics(t, func() {
132 removePidFile(pidFile)
133 })
134
135 // File should not exist after removal attempt
136 assert.NoFileExists(t, pidFile)
137 })
138 }
139}
140
141func TestCreatePidFile_Locations(t *testing.T) {
142 // Test that createPidFile tries multiple locations
143 // We can't easily mock os.WriteFile to test all failure scenarios,
144 // but we can verify the function doesn't panic
145 assert.NotPanics(t, func() {
146 pidFile := createPidFile()
147 if pidFile != "" {
148 removePidFile(pidFile)
149 }
150 })
151}
152
153func TestCreatePidFile_Content(t *testing.T) {
154 pidFile := createPidFile()
155 if pidFile == "" {
156 t.Skip("No writable location found for PID file")
157 }
158
159 defer removePidFile(pidFile)
160
161 // Verify PID file contains current process ID
162 content, err := os.ReadFile(pidFile)
163 require.NoError(t, err)
164
165 var storedPid int
166 _, err = fmt.Sscanf(string(content), "%d", &storedPid)
167 require.NoError(t, err)
168
169 assert.Equal(t, os.Getpid(), storedPid)
170}
171
172func TestPidFilePermissions(t *testing.T) {
173 pidFile := createPidFile()
174 if pidFile == "" {
175 t.Skip("No writable location found for PID file")
176 }
177
178 defer removePidFile(pidFile)
179
180 // Check file permissions
181 info, err := os.Stat(pidFile)
182 require.NoError(t, err)
183
184 // Should be readable by owner and group
185 assert.True(t, info.Mode().Perm()&0644 != 0)
186}
187
188func TestPidFileLocations(t *testing.T) {
189 // Test different potential PID file locations
190 // This mainly tests that the function handles various scenarios
191
192 originalHome := os.Getenv("HOME")
193 defer os.Setenv("HOME", originalHome)
194
195 // Test with invalid home directory
196 os.Setenv("HOME", "/nonexistent/invalid/path")
197
198 assert.NotPanics(t, func() {
199 pidFile := createPidFile()
200 if pidFile != "" {
201 removePidFile(pidFile)
202 }
203 })
204}
205
206func TestStartCmdStructure(t *testing.T) {
207 // Verify command is properly registered
208 found := false
209 for _, cmd := range rootCmd.Commands() {
210 if cmd.Name() == "start" {
211 found = true
212 break
213 }
214 }
215 assert.True(t, found, "start command should be registered with root command")
216}
217
218func TestStartCmdHelp(t *testing.T) {
219 var output bytes.Buffer
220
221 // Create a copy of the command to avoid affecting the global command
222 cmd := &cobra.Command{
223 Use: startCmd.Use,
224 Short: startCmd.Short,
225 Long: startCmd.Long,
226 Run: startCmd.Run,
227 }
228
229 cmd.SetOut(&output)
230 cmd.SetErr(&output)
231 cmd.SetArgs([]string{"--help"})
232
233 err := cmd.Execute()
234 assert.NoError(t, err)
235
236 helpOutput := output.String()
237 assert.Contains(t, helpOutput, "Start")
238 assert.Contains(t, helpOutput, "container registry")
239 assert.Contains(t, helpOutput, "reverse proxy")
240 assert.Contains(t, helpOutput, "server")
241}
242
243// Test helper functions for signal handling scenarios
244func TestStartCommand_SignalHandling(t *testing.T) {
245 // This is a simplified test since we can't easily test the full signal handling
246 // without actually starting the server. We mainly test that the command structure is correct.
247
248 // Verify the command can be created and has the right properties
249 assert.NotNil(t, startCmd.Run)
250 assert.Equal(t, "start", startCmd.Use)
251}
252
253func TestRunStart_ValidConfig(t *testing.T) {
254 // Create a valid config file
255 tempDir := t.TempDir()
256 configFile := filepath.Join(tempDir, "gordon.toml")
257
258 configContent := `
259[server]
260port = 8080
261registry_port = 5000
262runtime = "docker"
263
264[logging]
265enabled = false
266dir = "` + filepath.Join(tempDir, "logs") + `"
267
268[registry]
269enabled = false
270
271[routes]
272`
273
274 err := os.WriteFile(configFile, []byte(configContent), 0644)
275 require.NoError(t, err)
276
277 // Save original values
278 originalCfgFile := cfgFile
279 defer func() {
280 cfgFile = originalCfgFile
281 viper.Reset()
282 }()
283
284 // Set config file
285 cfgFile = configFile
286 viper.Reset()
287
288 // We can't actually run the full start command without Docker,
289 // but we can test that it doesn't immediately fail with config errors
290 // by mocking the execution
291
292 // This is a limited test since runStart calls log.Fatal on various errors
293 // and we can't easily test the full server startup without actual dependencies
294 assert.NotNil(t, startCmd.Run, "Start command should have a run function")
295}
296
297func TestSignalHandling_Concepts(t *testing.T) {
298 // Test the concepts used in signal handling
299
300 // Test that we can create signal channels (used in runStart)
301 sigChan := make(chan os.Signal, 1)
302 reloadChan := make(chan os.Signal, 1)
303
304 assert.NotNil(t, sigChan)
305 assert.NotNil(t, reloadChan)
306
307 // Test signal types that would be used
308 signals := []os.Signal{
309 syscall.SIGINT,
310 syscall.SIGTERM,
311 syscall.SIGUSR1,
312 }
313
314 for _, sig := range signals {
315 assert.NotNil(t, sig)
316 }
317}
318
319func TestContextCancellation(t *testing.T) {
320 // Test context cancellation patterns used in runStart
321 ctx, cancel := context.WithCancel(context.Background())
322 assert.NotNil(t, ctx)
323 assert.NotNil(t, cancel)
324
325 // Test timeout context
326 timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 30*time.Second)
327 assert.NotNil(t, timeoutCtx)
328 assert.NotNil(t, timeoutCancel)
329
330 // Clean up
331 cancel()
332 timeoutCancel()
333}