1package executor
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/bnema/archup/internal/domain/ports/mocks"
10 "go.uber.org/mock/gomock"
11)
12
13func TestShellExecutor_Execute_Success(t *testing.T) {
14 ctrl := gomock.NewController(t)
15 defer ctrl.Finish()
16 mockLogger := mocks.NewMockLogger(ctrl)
17 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
18 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
19 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
20 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
21
22 executor := NewShellExecutor(mockLogger)
23 ctx := context.Background()
24
25 output, err := executor.Execute(ctx, "echo", "hello world")
26 if err != nil {
27 t.Fatalf("expected no error, got %v", err)
28 }
29
30 expected := "hello world\n"
31 if string(output) != expected {
32 t.Errorf("expected %q, got %q", expected, string(output))
33 }
34}
35
36func TestShellExecutor_Execute_WithArgs(t *testing.T) {
37 ctrl := gomock.NewController(t)
38 defer ctrl.Finish()
39 mockLogger := mocks.NewMockLogger(ctrl)
40 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
41 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
42 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
43 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
44
45 executor := NewShellExecutor(mockLogger)
46 ctx := context.Background()
47
48 // Test with multiple arguments
49 output, err := executor.Execute(ctx, "echo", "arg1", "arg2", "arg3")
50 if err != nil {
51 t.Fatalf("expected no error, got %v", err)
52 }
53
54 if !strings.Contains(string(output), "arg1") {
55 t.Errorf("expected arg1 in output, got %s", string(output))
56 }
57}
58
59func TestShellExecutor_Execute_CommandNotFound(t *testing.T) {
60 ctrl := gomock.NewController(t)
61 defer ctrl.Finish()
62 mockLogger := mocks.NewMockLogger(ctrl)
63 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
64 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
65 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
66 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
67
68 executor := NewShellExecutor(mockLogger)
69 ctx := context.Background()
70
71 _, err := executor.Execute(ctx, "nonexistentcommand12345", "arg")
72 if err == nil {
73 t.Fatal("expected error for nonexistent command")
74 }
75}
76
77func TestShellExecutor_Execute_CommandFailure(t *testing.T) {
78 ctrl := gomock.NewController(t)
79 defer ctrl.Finish()
80 mockLogger := mocks.NewMockLogger(ctrl)
81 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
82 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
83 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
84 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
85
86 executor := NewShellExecutor(mockLogger)
87 ctx := context.Background()
88
89 // false command always exits with code 1
90 _, err := executor.Execute(ctx, "false")
91 if err == nil {
92 t.Fatal("expected error for failed command")
93 }
94}
95
96func TestShellExecutor_ExecuteWithEnv_Success(t *testing.T) {
97 ctrl := gomock.NewController(t)
98 defer ctrl.Finish()
99 mockLogger := mocks.NewMockLogger(ctrl)
100 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
101 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
102 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
103 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
104
105 executor := NewShellExecutor(mockLogger)
106 ctx := context.Background()
107
108 env := map[string]string{
109 "TEST_VAR": "test_value",
110 }
111
112 // Echo the environment variable
113 output, err := executor.ExecuteWithEnv(ctx, env, "sh", "-c", "echo $TEST_VAR")
114 if err != nil {
115 t.Fatalf("expected no error, got %v", err)
116 }
117
118 if !strings.Contains(string(output), "test_value") {
119 t.Errorf("expected test_value in output, got %s", string(output))
120 }
121}
122
123func TestShellExecutor_ExecuteWithEnv_MultipleVars(t *testing.T) {
124 ctrl := gomock.NewController(t)
125 defer ctrl.Finish()
126 mockLogger := mocks.NewMockLogger(ctrl)
127 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
128 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
129 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
130 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
131
132 executor := NewShellExecutor(mockLogger)
133 ctx := context.Background()
134
135 env := map[string]string{
136 "VAR1": "value1",
137 "VAR2": "value2",
138 }
139
140 output, err := executor.ExecuteWithEnv(ctx, env, "sh", "-c", "echo $VAR1 $VAR2")
141 if err != nil {
142 t.Fatalf("expected no error, got %v", err)
143 }
144
145 output_str := string(output)
146 if !strings.Contains(output_str, "value1") || !strings.Contains(output_str, "value2") {
147 t.Errorf("expected both values in output, got %s", output_str)
148 }
149}
150
151func TestShellExecutor_ExecuteWithStdin_Success(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 := NewShellExecutor(mockLogger)
161 ctx := context.Background()
162
163 output, err := executor.ExecuteWithStdin(ctx, "hello stdin", "cat")
164 if err != nil {
165 t.Fatalf("expected no error, got %v", err)
166 }
167
168 if string(output) != "hello stdin" {
169 t.Errorf("expected stdin to be forwarded, got %q", string(output))
170 }
171}
172
173func TestShellExecutor_Execute_ContextCancellation(t *testing.T) {
174 ctrl := gomock.NewController(t)
175 defer ctrl.Finish()
176 mockLogger := mocks.NewMockLogger(ctrl)
177 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
178 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
179 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
180 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
181
182 executor := NewShellExecutor(mockLogger)
183 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
184 defer cancel()
185
186 // sleep command will be interrupted
187 _, err := executor.Execute(ctx, "sleep", "10")
188 if err == nil {
189 t.Fatal("expected error due to context cancellation")
190 }
191}
192
193func TestShellExecutor_Execute_EmptyOutput(t *testing.T) {
194 ctrl := gomock.NewController(t)
195 defer ctrl.Finish()
196 mockLogger := mocks.NewMockLogger(ctrl)
197 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
198 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
199 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
200 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
201
202 executor := NewShellExecutor(mockLogger)
203 ctx := context.Background()
204
205 // true command produces no output
206 output, err := executor.Execute(ctx, "true")
207 if err != nil {
208 t.Fatalf("expected no error, got %v", err)
209 }
210
211 if len(output) != 0 {
212 t.Errorf("expected empty output, got %s", string(output))
213 }
214}
215
216func TestShellExecutor_Execute_StderrCapture(t *testing.T) {
217 ctrl := gomock.NewController(t)
218 defer ctrl.Finish()
219 mockLogger := mocks.NewMockLogger(ctrl)
220 mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
221 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
222 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
223 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
224
225 executor := NewShellExecutor(mockLogger)
226 ctx := context.Background()
227
228 // sh command that writes to stderr
229 _, err := executor.Execute(ctx, "sh", "-c", "echo error >&2; exit 1")
230 if err == nil {
231 t.Fatal("expected error")
232 }
233
234 // Verify stderr is captured in error message
235 if !strings.Contains(err.Error(), "error") {
236 t.Errorf("expected 'error' in error message, got %s", err.Error())
237 }
238}