detached 6af0a61

chroot_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close