1package cmd
2
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "path/filepath"
8 "syscall"
9 "testing"
10
11 "github.com/spf13/cobra"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14)
15
16func TestReloadCommand(t *testing.T) {
17 // Test reload command structure
18 assert.Equal(t, "reload", reloadCmd.Use)
19 assert.Contains(t, reloadCmd.Short, "Reload")
20 assert.Contains(t, reloadCmd.Long, "configuration")
21 assert.Contains(t, reloadCmd.Long, "environment variables")
22}
23
24func TestReloadCmdStructure(t *testing.T) {
25 // Verify command is properly registered
26 found := false
27 for _, cmd := range rootCmd.Commands() {
28 if cmd.Name() == "reload" {
29 found = true
30 break
31 }
32 }
33 assert.True(t, found, "reload command should be registered with root command")
34}
35
36func TestReloadCmdHelp(t *testing.T) {
37 var output bytes.Buffer
38
39 // Create a copy of the command to avoid affecting the global command
40 cmd := &cobra.Command{
41 Use: reloadCmd.Use,
42 Short: reloadCmd.Short,
43 Long: reloadCmd.Long,
44 Run: reloadCmd.Run,
45 }
46
47 cmd.SetOut(&output)
48 cmd.SetErr(&output)
49 cmd.SetArgs([]string{"--help"})
50
51 err := cmd.Execute()
52 assert.NoError(t, err)
53
54 helpOutput := output.String()
55 assert.Contains(t, helpOutput, "Reload")
56 assert.Contains(t, helpOutput, "configuration")
57 assert.Contains(t, helpOutput, "environment variables")
58 assert.Contains(t, helpOutput, "containers")
59}
60
61func TestFindGordonPidFile(t *testing.T) {
62 tests := []struct {
63 name string
64 setupFunc func(t *testing.T) (string, func()) // returns temp dir and cleanup func
65 expectedFound bool
66 }{
67 {
68 name: "PID file exists in temp directory",
69 setupFunc: func(t *testing.T) (string, func()) {
70 tempDir := t.TempDir()
71 pidFile := filepath.Join(tempDir, "gordon.pid")
72 err := os.WriteFile(pidFile, []byte("12345"), 0644)
73 require.NoError(t, err)
74
75 cleanup := func() {
76 // Cleanup function for consistency
77 }
78 return pidFile, cleanup
79 },
80 expectedFound: false, // Won't find it because we can't easily mock os.TempDir
81 },
82 {
83 name: "PID file exists in home directory",
84 setupFunc: func(t *testing.T) (string, func()) {
85 // Create temp directory to simulate home
86 tempDir := t.TempDir()
87 pidFile := filepath.Join(tempDir, ".gordon.pid")
88 err := os.WriteFile(pidFile, []byte("12345"), 0644)
89 require.NoError(t, err)
90
91 // Mock home directory
92 originalHome := os.Getenv("HOME")
93 os.Setenv("HOME", tempDir)
94
95 cleanup := func() {
96 os.Setenv("HOME", originalHome)
97 }
98 return pidFile, cleanup
99 },
100 expectedFound: true,
101 },
102 {
103 name: "no PID file exists",
104 setupFunc: func(t *testing.T) (string, func()) {
105 // Set home to a temp directory with no PID file
106 tempDir := t.TempDir()
107 originalHome := os.Getenv("HOME")
108 os.Setenv("HOME", tempDir)
109
110 cleanup := func() {
111 os.Setenv("HOME", originalHome)
112 }
113 return "", cleanup
114 },
115 expectedFound: false,
116 },
117 }
118
119 for _, tt := range tests {
120 t.Run(tt.name, func(t *testing.T) {
121 expectedPidFile, cleanup := tt.setupFunc(t)
122 defer cleanup()
123
124 result := findGordonPidFile()
125
126 if tt.expectedFound {
127 assert.NotEmpty(t, result)
128 assert.Equal(t, expectedPidFile, result)
129 } else {
130 assert.Empty(t, result)
131 }
132 })
133 }
134}
135
136func TestRunReload_NoPidFile(t *testing.T) {
137 // Set up environment with no PID file
138 tempDir := t.TempDir()
139 originalHome := os.Getenv("HOME")
140 os.Setenv("HOME", tempDir)
141 defer os.Setenv("HOME", originalHome)
142
143 // Capture stdout by redirecting it
144 oldStdout := os.Stdout
145 r, w, _ := os.Pipe()
146 os.Stdout = w
147
148 done := make(chan string)
149 go func() {
150 var buf bytes.Buffer
151 buf.ReadFrom(r)
152 done <- buf.String()
153 }()
154
155 // Run reload command
156 runReload(reloadCmd, []string{})
157
158 // Restore stdout and get output
159 w.Close()
160 os.Stdout = oldStdout
161 outputStr := <-done
162
163 // Should indicate PID file not found
164 assert.Contains(t, outputStr, "PID file not found")
165 assert.Contains(t, outputStr, "Is Gordon running?")
166}
167
168func TestRunReload_WithValidPidFile(t *testing.T) {
169 // Create a test PID file
170 tempDir := t.TempDir()
171 pidFile := filepath.Join(tempDir, ".gordon.pid")
172
173 // Write current process PID to file (so it's valid)
174 pid := os.Getpid()
175 err := os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", pid)), 0644)
176 require.NoError(t, err)
177
178 // Mock home directory
179 originalHome := os.Getenv("HOME")
180 os.Setenv("HOME", tempDir)
181 defer os.Setenv("HOME", originalHome)
182
183 // Capture stdout
184 var output bytes.Buffer
185 reloadCmd.SetOut(&output)
186
187 // We can't easily test the actual signal sending to another process,
188 // but we can test the PID file reading logic
189
190 // Test PID file reading
191 foundPidFile := findGordonPidFile()
192 assert.Equal(t, pidFile, foundPidFile)
193
194 // Test PID parsing
195 pidBytes, err := os.ReadFile(pidFile)
196 require.NoError(t, err)
197
198 var parsedPid int
199 _, err = fmt.Sscanf(string(pidBytes), "%d", &parsedPid)
200 require.NoError(t, err)
201 assert.Equal(t, pid, parsedPid)
202}
203
204func TestRunReload_InvalidPidFile(t *testing.T) {
205 // Create a PID file with invalid content
206 tempDir := t.TempDir()
207 pidFile := filepath.Join(tempDir, ".gordon.pid")
208
209 // Write invalid PID content
210 err := os.WriteFile(pidFile, []byte("invalid_pid"), 0644)
211 require.NoError(t, err)
212
213 // Mock home directory
214 originalHome := os.Getenv("HOME")
215 os.Setenv("HOME", tempDir)
216 defer os.Setenv("HOME", originalHome)
217
218 // Capture stdout by redirecting it
219 oldStdout := os.Stdout
220 r, w, _ := os.Pipe()
221 os.Stdout = w
222
223 done := make(chan string)
224 go func() {
225 var buf bytes.Buffer
226 buf.ReadFrom(r)
227 done <- buf.String()
228 }()
229
230 // Run reload command
231 runReload(reloadCmd, []string{})
232
233 // Restore stdout and get output
234 w.Close()
235 os.Stdout = oldStdout
236 outputStr := <-done
237
238 // Should indicate parsing error
239 assert.Contains(t, outputStr, "Error parsing Gordon PID")
240}
241
242func TestRunReload_UnreadablePidFile(t *testing.T) {
243 // Create a PID file that can't be read
244 tempDir := t.TempDir()
245 pidFile := filepath.Join(tempDir, ".gordon.pid")
246
247 // Create file but make it unreadable (if possible)
248 err := os.WriteFile(pidFile, []byte("12345"), 0000)
249 require.NoError(t, err)
250
251 // Mock home directory
252 originalHome := os.Getenv("HOME")
253 os.Setenv("HOME", tempDir)
254 defer os.Setenv("HOME", originalHome)
255
256 // Capture stdout
257 var output bytes.Buffer
258 reloadCmd.SetOut(&output)
259
260 // Run reload command
261 runReload(reloadCmd, []string{})
262
263 // Should indicate read error (on systems that enforce permissions)
264 outputStr := output.String()
265 // Note: This test might not fail on all systems due to permission handling
266 if len(outputStr) > 0 {
267 // If we get output, it should contain an error message
268 assert.True(t,
269 contains(outputStr, "Error reading") ||
270 contains(outputStr, "Error parsing") ||
271 contains(outputStr, "Error finding") ||
272 contains(outputStr, "Error sending"),
273 "Should contain some error message, got: %s", outputStr)
274 }
275}
276
277func TestRunReload_NonExistentProcess(t *testing.T) {
278 // Create a PID file with a PID that doesn't exist
279 tempDir := t.TempDir()
280 pidFile := filepath.Join(tempDir, ".gordon.pid")
281
282 // Use a PID that's very unlikely to exist (high number)
283 nonExistentPid := 999999
284 err := os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", nonExistentPid)), 0644)
285 require.NoError(t, err)
286
287 // Mock home directory
288 originalHome := os.Getenv("HOME")
289 os.Setenv("HOME", tempDir)
290 defer os.Setenv("HOME", originalHome)
291
292 // Capture stdout by redirecting it
293 oldStdout := os.Stdout
294 r, w, _ := os.Pipe()
295 os.Stdout = w
296
297 done := make(chan string)
298 go func() {
299 var buf bytes.Buffer
300 buf.ReadFrom(r)
301 done <- buf.String()
302 }()
303
304 // Run reload command
305 runReload(reloadCmd, []string{})
306
307 // Restore stdout and get output
308 w.Close()
309 os.Stdout = oldStdout
310 outputStr := <-done
311
312 // Should indicate process not found or signal sending error
313 assert.True(t,
314 contains(outputStr, "Error finding") ||
315 contains(outputStr, "Error sending") ||
316 contains(outputStr, "not be running"),
317 "Should contain process-related error, got: %s", outputStr)
318}
319
320func TestFindGordonPidFile_AllLocations(t *testing.T) {
321 // Test the locations that findGordonPidFile checks
322 locations := []string{
323 "/tmp/gordon.pid",
324 "/var/run/gordon.pid",
325 }
326
327 // Add temp dir location
328 tempDirLocation := filepath.Join(os.TempDir(), "gordon.pid")
329 locations = append(locations, tempDirLocation)
330
331 // Add home directory location
332 if homeDir, err := os.UserHomeDir(); err == nil {
333 homeLocation := filepath.Join(homeDir, ".gordon.pid")
334 locations = append(locations, homeLocation)
335 }
336
337 // Verify that the function checks these locations
338 // We can't easily test all of them without root access,
339 // but we can verify the function doesn't panic
340 assert.NotPanics(t, func() {
341 result := findGordonPidFile()
342 // Result might be empty if no PID file exists, which is fine
343 _ = result
344 })
345}
346
347func TestSignalSending_Concepts(t *testing.T) {
348 // Test the signal sending concepts used in runReload
349
350 // Test that we can find our own process
351 process, err := os.FindProcess(os.Getpid())
352 assert.NoError(t, err)
353 assert.NotNil(t, process)
354
355 // Test signal types
356 signals := []syscall.Signal{
357 syscall.SIGUSR1,
358 syscall.SIGTERM,
359 syscall.SIGINT,
360 }
361
362 for _, sig := range signals {
363 assert.NotNil(t, sig)
364 }
365}
366
367func TestPidFileValidation(t *testing.T) {
368 tests := []struct {
369 name string
370 content string
371 expectErr bool
372 }{
373 {
374 name: "valid PID",
375 content: "12345",
376 expectErr: false,
377 },
378 {
379 name: "invalid PID - letters",
380 content: "abc",
381 expectErr: true,
382 },
383 {
384 name: "invalid PID - empty",
385 content: "",
386 expectErr: true,
387 },
388 {
389 name: "invalid PID - negative",
390 content: "-123",
391 expectErr: false, // Parsing succeeds but PID is invalid
392 },
393 {
394 name: "valid PID with whitespace",
395 content: " 12345 \n",
396 expectErr: false,
397 },
398 }
399
400 for _, tt := range tests {
401 t.Run(tt.name, func(t *testing.T) {
402 var pid int
403 _, err := fmt.Sscanf(tt.content, "%d", &pid)
404
405 if tt.expectErr {
406 assert.Error(t, err)
407 } else {
408 assert.NoError(t, err)
409 if tt.content != "" && tt.content != " 12345 \n" && tt.content != "-123" {
410 assert.Greater(t, pid, 0)
411 }
412 }
413 })
414 }
415}
416
417// Helper function to check if string contains substring (case-insensitive)
418func contains(s, substr string) bool {
419 return len(s) >= len(substr) &&
420 (s == substr ||
421 len(s) > len(substr) &&
422 (s[:len(substr)] == substr ||
423 s[len(s)-len(substr):] == substr ||
424 searchSubstring(s, substr)))
425}
426
427func searchSubstring(s, substr string) bool {
428 for i := 0; i <= len(s)-len(substr); i++ {
429 if s[i:i+len(substr)] == substr {
430 return true
431 }
432 }
433 return false
434}