1package executor
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "github.com/bnema/archup/internal/domain/ports/mocks"
11 "go.uber.org/mock/gomock"
12)
13
14func TestChrootExecutor_NewChrootExecutor(t *testing.T) {
15 ctrl := gomock.NewController(t)
16 defer ctrl.Finish()
17 mockLogger := mocks.NewMockLogger(ctrl)
18 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
19 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
20 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
21 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
22
23 executor := NewChrootExecutor(mockLogger)
24 if executor == nil {
25 t.Fatal("expected non-nil executor")
26 }
27}
28
29func TestChrootExecutor_ExecuteInChroot_PathNotExists(t *testing.T) {
30 ctrl := gomock.NewController(t)
31 defer ctrl.Finish()
32 mockLogger := mocks.NewMockLogger(ctrl)
33 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
34 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
35 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
36 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
37
38 executor := NewChrootExecutor(mockLogger)
39 ctx := context.Background()
40
41 _, err := executor.ExecuteInChroot(ctx, "/nonexistent/path", "echo", "test")
42 if err == nil {
43 t.Fatal("expected error for nonexistent chroot path")
44 }
45}
46
47func TestChrootExecutor_ExecuteInChroot_CommandNotFound(t *testing.T) {
48 ctrl := gomock.NewController(t)
49 defer ctrl.Finish()
50 mockLogger := mocks.NewMockLogger(ctrl)
51 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
52 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
53 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
54 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
55
56 executor := NewChrootExecutor(mockLogger)
57 ctx := context.Background()
58
59 // Create temporary directory to use as chroot
60 tmpDir := t.TempDir()
61
62 // arch-chroot requires proper setup which we can't do in unit tests,
63 // so we expect this to fail when trying to execute arch-chroot
64 _, err := executor.ExecuteInChroot(ctx, tmpDir, "echo", "test")
65 if err == nil {
66 t.Fatal("expected error when executing in non-functional chroot")
67 }
68}
69
70func TestChrootExecutor_ChrootSystemctl_PathNotExists(t *testing.T) {
71 ctrl := gomock.NewController(t)
72 defer ctrl.Finish()
73 mockLogger := mocks.NewMockLogger(ctrl)
74 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
75 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
76 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
77 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
78
79 executor := NewChrootExecutor(mockLogger)
80
81 err := executor.ChrootSystemctl(context.Background(), "", "/nonexistent/path", "enable", "service")
82 if err == nil {
83 t.Fatal("expected error for nonexistent chroot path")
84 }
85}
86
87func TestChrootExecutor_ChrootSystemctl_CreatesLogDirectory(t *testing.T) {
88 ctrl := gomock.NewController(t)
89 defer ctrl.Finish()
90 mockLogger := mocks.NewMockLogger(ctrl)
91 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
92 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
93 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
94 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
95
96 executor := NewChrootExecutor(mockLogger)
97 tmpDir := t.TempDir()
98 logPath := filepath.Join(tmpDir, "subdir", "log.txt")
99
100 // This will fail due to arch-chroot not being available,
101 // but the directory should be created
102 _ = executor.ChrootSystemctl(context.Background(), logPath, tmpDir, "enable", "service")
103
104 // Check if log directory was created
105 logDir := filepath.Dir(logPath)
106 if _, err := os.Stat(logDir); os.IsNotExist(err) {
107 t.Error("expected log directory to be created")
108 }
109}
110
111func TestChrootExecutor_ChrootSystemctl_LogsErrors(t *testing.T) {
112 ctrl := gomock.NewController(t)
113 defer ctrl.Finish()
114 mockLogger := mocks.NewMockLogger(ctrl)
115 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
116 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
117 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
118 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
119
120 executor := NewChrootExecutor(mockLogger)
121 tmpDir := t.TempDir()
122 logPath := filepath.Join(tmpDir, "log.txt")
123
124 // This will fail due to arch-chroot not being available
125 _ = executor.ChrootSystemctl(context.Background(), logPath, tmpDir, "enable", "service")
126
127 // Check if log file was created
128 if _, err := os.Stat(logPath); os.IsNotExist(err) {
129 // It's OK if log file wasn't created due to error
130 t.Log("log file not created (expected on failure)")
131 }
132}
133
134func TestChrootExecutor_ChrootSystemctl_EmptyLogPath(t *testing.T) {
135 ctrl := gomock.NewController(t)
136 defer ctrl.Finish()
137 mockLogger := mocks.NewMockLogger(ctrl)
138 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
139 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
140 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
141 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
142
143 executor := NewChrootExecutor(mockLogger)
144 tmpDir := t.TempDir()
145
146 // This should not panic even with empty log path
147 _ = executor.ChrootSystemctl(context.Background(), "", tmpDir, "enable", "service")
148}
149
150func TestChrootExecutor_ExecuteInChrootWithStdin(t *testing.T) {
151 t.Run("passes-stdin", func(t *testing.T) {
152 ctrl := gomock.NewController(t)
153 defer ctrl.Finish()
154 mockLogger := mocks.NewMockLogger(ctrl)
155 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
156 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
157 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
158 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
159
160 executor := NewChrootExecutor(mockLogger)
161 workspace := t.TempDir()
162 chrootPath := filepath.Join(workspace, "chroot")
163 if err := os.MkdirAll(chrootPath, 0755); err != nil {
164 t.Fatalf("failed to create chroot path: %v", err)
165 }
166
167 scriptDir := filepath.Join(workspace, "bin")
168 if err := os.MkdirAll(scriptDir, 0755); err != nil {
169 t.Fatalf("failed to create script dir: %v", err)
170 }
171
172 outputPath := filepath.Join(workspace, "stdin.txt")
173 scriptPath := filepath.Join(scriptDir, "arch-chroot")
174 script := "#!/bin/sh\ncat - > \"$OUTPUT_FILE\"\n"
175 if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
176 t.Fatalf("failed to write arch-chroot script: %v", err)
177 }
178
179 oldPath := os.Getenv("PATH")
180 if err := os.Setenv("PATH", scriptDir+":"+oldPath); err != nil {
181 t.Fatalf("failed to set PATH: %v", err)
182 }
183 if err := os.Setenv("OUTPUT_FILE", outputPath); err != nil {
184 t.Fatalf("failed to set OUTPUT_FILE: %v", err)
185 }
186 t.Cleanup(func() {
187 _ = os.Setenv("PATH", oldPath)
188 _ = os.Unsetenv("OUTPUT_FILE")
189 })
190
191 if err := executor.ExecuteInChrootWithStdin(context.Background(), chrootPath, "hello", "echo", "ignored"); err != nil {
192 t.Fatalf("expected no error, got %v", err)
193 }
194
195 content, err := os.ReadFile(outputPath)
196 if err != nil {
197 t.Fatalf("failed to read output file: %v", err)
198 }
199 if string(content) != "hello" {
200 t.Errorf("expected stdin to be written, got %q", string(content))
201 }
202 })
203
204 t.Run("context-cancel", func(t *testing.T) {
205 ctrl := gomock.NewController(t)
206 defer ctrl.Finish()
207 mockLogger := mocks.NewMockLogger(ctrl)
208 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
209 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
210 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
211 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
212
213 executor := NewChrootExecutor(mockLogger)
214 workspace := t.TempDir()
215 chrootPath := filepath.Join(workspace, "chroot")
216 if err := os.MkdirAll(chrootPath, 0755); err != nil {
217 t.Fatalf("failed to create chroot path: %v", err)
218 }
219
220 scriptDir := filepath.Join(workspace, "bin")
221 if err := os.MkdirAll(scriptDir, 0755); err != nil {
222 t.Fatalf("failed to create script dir: %v", err)
223 }
224
225 scriptPath := filepath.Join(scriptDir, "arch-chroot")
226 script := "#!/bin/sh\nsleep 5\n"
227 if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
228 t.Fatalf("failed to write arch-chroot script: %v", err)
229 }
230
231 oldPath := os.Getenv("PATH")
232 if err := os.Setenv("PATH", scriptDir+":"+oldPath); err != nil {
233 t.Fatalf("failed to set PATH: %v", err)
234 }
235 t.Cleanup(func() {
236 _ = os.Setenv("PATH", oldPath)
237 })
238
239 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
240 defer cancel()
241 if err := executor.ExecuteInChrootWithStdin(ctx, chrootPath, "hello", "sleep", "5"); err == nil {
242 t.Fatal("expected error due to context cancellation")
243 }
244 })
245
246 t.Run("missing-chroot-path", func(t *testing.T) {
247 ctrl := gomock.NewController(t)
248 defer ctrl.Finish()
249 mockLogger := mocks.NewMockLogger(ctrl)
250 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
251 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
252 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
253 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
254
255 executor := NewChrootExecutor(mockLogger)
256 if err := executor.ExecuteInChrootWithStdin(context.Background(), "/nonexistent/path", "input", "echo", "test"); err == nil {
257 t.Fatal("expected error for nonexistent chroot path")
258 }
259 })
260}