main 6af0a61

post_install_handler_test.go RawHistoryFolder

1package handlers
2
3import (
4	"context"
5	"fmt"
6	"net/http"
7	"strings"
8	"testing"
9
10	"github.com/bnema/archup/internal/application/commands"
11	"github.com/bnema/archup/internal/domain/ports"
12	"github.com/bnema/archup/internal/domain/ports/mocks"
13	"go.uber.org/mock/gomock"
14)
15
16func newMockResponse(ctrl *gomock.Controller, statusCode int, body []byte) ports.Response {
17	resp := mocks.NewMockResponse(ctrl)
18	resp.EXPECT().StatusCode().Return(statusCode).AnyTimes()
19	resp.EXPECT().Body().Return(body).AnyTimes()
20	resp.EXPECT().Close().Return(nil).AnyTimes()
21	return resp
22}
23
24func TestPostInstallHandler_Handle_NoScripts(t *testing.T) {
25	ctrl := gomock.NewController(t)
26	defer ctrl.Finish()
27
28	mockFS := mocks.NewMockFileSystem(ctrl)
29	mockHTTP := mocks.NewMockHTTPClient(ctrl)
30	mockChrExec := mocks.NewMockChrootExecutor(ctrl)
31	mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
32	mockLogger := mocks.NewMockLogger(ctrl)
33
34	mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
35	mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
36	mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
37	mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
38	mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
39	mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
40	mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
41	mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
42	mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
43	mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
44	mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
45
46	handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
47
48	cmd := commands.PostInstallCommand{
49		MountPoint:         "/mnt",
50		Username:           "testuser",
51		RunPostBootScripts: false,
52		PlymouthTheme:      "",
53	}
54
55	result, err := handler.Handle(context.Background(), cmd)
56
57	if err != nil {
58		t.Fatalf("expected no error, got %v", err)
59	}
60
61	if !result.Success {
62		t.Error("expected success")
63	}
64
65	if len(result.TasksRun) != 0 {
66		t.Errorf("expected no tasks run, got %d", len(result.TasksRun))
67	}
68}
69
70func TestPostInstallHandler_Handle_WithScripts(t *testing.T) {
71	ctrl := gomock.NewController(t)
72	defer ctrl.Finish()
73
74	mockFS := mocks.NewMockFileSystem(ctrl)
75	mockHTTP := mocks.NewMockHTTPClient(ctrl)
76	mockChrExec := mocks.NewMockChrootExecutor(ctrl)
77	mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
78	mockLogger := mocks.NewMockLogger(ctrl)
79
80	mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
81	mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
82	mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
83	mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
84	mockFS.EXPECT().Exists(gomock.Any()).Return(true, nil).AnyTimes()
85	mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("content"), nil).AnyTimes()
86	mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
87	mockFS.EXPECT().Chmod(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
88	mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
89	mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
90	mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
91	mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
92
93	handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
94
95	cmd := commands.PostInstallCommand{
96		MountPoint:         "/mnt",
97		Username:           "testuser",
98		RunPostBootScripts: true,
99		PlymouthTheme:      "",
100	}
101
102	result, err := handler.Handle(context.Background(), cmd)
103
104	if err != nil {
105		t.Fatalf("expected no error, got %v", err)
106	}
107
108	if !result.Success {
109		t.Error("expected success")
110	}
111
112	if len(result.TasksRun) != 1 {
113		t.Errorf("expected 1 task run, got %d", len(result.TasksRun))
114	}
115
116	if result.TasksRun[0] != "post-boot-scripts" {
117		t.Errorf("expected task 'post-boot-scripts', got %s", result.TasksRun[0])
118	}
119}
120
121func TestPostInstallHandler_Handle_WithPlymouth(t *testing.T) {
122	ctrl := gomock.NewController(t)
123	defer ctrl.Finish()
124
125	mockFS := mocks.NewMockFileSystem(ctrl)
126	mockHTTP := mocks.NewMockHTTPClient(ctrl)
127	mockChrExec := mocks.NewMockChrootExecutor(ctrl)
128	mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
129	mockLogger := mocks.NewMockLogger(ctrl)
130
131	mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
132	mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
133	mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
134	mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
135	mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
136	mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
137	mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
138	mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
139	mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
140	mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
141	mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
142
143	handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
144
145	cmd := commands.PostInstallCommand{
146		MountPoint:         "/mnt",
147		Username:           "testuser",
148		RunPostBootScripts: false,
149		PlymouthTheme:      "spinner",
150	}
151
152	result, err := handler.Handle(context.Background(), cmd)
153
154	if err != nil {
155		t.Fatalf("expected no error, got %v", err)
156	}
157
158	if !result.Success {
159		t.Error("expected success")
160	}
161
162	if len(result.TasksRun) != 1 {
163		t.Errorf("expected 1 task run, got %d", len(result.TasksRun))
164	}
165
166	if result.TasksRun[0] != "plymouth-theme-spinner" {
167		t.Errorf("expected task 'plymouth-theme-spinner', got %s", result.TasksRun[0])
168	}
169}
170
171func TestPostInstallHandler_Handle_WithDankLinux(t *testing.T) {
172	ctrl := gomock.NewController(t)
173	defer ctrl.Finish()
174
175	mockFS := mocks.NewMockFileSystem(ctrl)
176	mockHTTP := mocks.NewMockHTTPClient(ctrl)
177	mockChrExec := mocks.NewMockChrootExecutor(ctrl)
178	mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
179	mockLogger := mocks.NewMockLogger(ctrl)
180
181	mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
182	mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
183	mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
184	mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
185	mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
186	mockFS.EXPECT().WriteFile(gomock.Eq("/mnt/var/lib/archup-install-danklinux"), gomock.Any(), gomock.Any()).Return(nil)
187	mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
188	mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
189	mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
190	mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
191	mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
192	mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
193
194	handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
195
196	cmd := commands.PostInstallCommand{
197		MountPoint:         "/mnt",
198		Username:           "testuser",
199		RunPostBootScripts: false,
200		PlymouthTheme:      "",
201		InstallDankLinux:   true,
202	}
203
204	result, err := handler.Handle(context.Background(), cmd)
205
206	if err != nil {
207		t.Fatalf("expected no error, got %v", err)
208	}
209
210	if !result.Success {
211		t.Error("expected success")
212	}
213
214	if len(result.TasksRun) != 1 {
215		t.Errorf("expected 1 task run, got %d", len(result.TasksRun))
216	}
217
218	if result.TasksRun[0] != "dank-linux-flag" {
219		t.Errorf("expected task 'dank-linux-flag', got %s", result.TasksRun[0])
220	}
221}
222
223func TestPostInstallHandler_Handle_TunesPacman(t *testing.T) {
224	ctrl := gomock.NewController(t)
225	defer ctrl.Finish()
226
227	mockFS := mocks.NewMockFileSystem(ctrl)
228	mockHTTP := mocks.NewMockHTTPClient(ctrl)
229	mockChrExec := mocks.NewMockChrootExecutor(ctrl)
230	mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
231	mockLogger := mocks.NewMockLogger(ctrl)
232
233	pacmanContent := "#Color\n#ParallelDownloads = 5\n#ILoveCandy\n"
234
235	mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
236	mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
237	mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
238	mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
239	mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
240	mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
241	mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
242	mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
243	mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
244
245	// ReadFile for pacman.conf returns commented options; other reads return generic content
246	mockFS.EXPECT().ReadFile(gomock.Eq("/mnt/etc/pacman.conf")).Return([]byte(pacmanContent), nil).AnyTimes()
247	mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
248
249	// Capture the WriteFile call for pacman.conf and assert content
250	mockFS.EXPECT().WriteFile(gomock.Eq("/mnt/etc/pacman.conf"), gomock.Any(), gomock.Any()).DoAndReturn(
251		func(path string, data []byte, perm interface{}) error {
252			content := string(data)
253			if !strings.Contains(content, "Color") {
254				t.Errorf("expected tuned pacman.conf to contain 'Color', got: %s", content)
255			}
256			if strings.Contains(content, "#Color") {
257				t.Errorf("expected '#Color' to be uncommented in pacman.conf")
258			}
259			if !strings.Contains(content, "ParallelDownloads = 5") {
260				t.Errorf("expected tuned pacman.conf to contain 'ParallelDownloads = 5'")
261			}
262			if !strings.Contains(content, "ILoveCandy") {
263				t.Errorf("expected tuned pacman.conf to contain 'ILoveCandy'")
264			}
265			return nil
266		},
267	).AnyTimes()
268	mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
269
270	handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
271
272	cmd := commands.PostInstallCommand{
273		MountPoint:         "/mnt",
274		Username:           "testuser",
275		RunPostBootScripts: false,
276		PlymouthTheme:      "",
277	}
278
279	result, err := handler.Handle(context.Background(), cmd)
280
281	if err != nil {
282		t.Fatalf("expected no error, got %v", err)
283	}
284
285	if !result.Success {
286		t.Error("expected success")
287	}
288}
289
290func TestPostInstallHandler_Handle_InstallsLimineHook(t *testing.T) {
291	ctrl := gomock.NewController(t)
292	defer ctrl.Finish()
293
294	mockFS := mocks.NewMockFileSystem(ctrl)
295	mockHTTP := mocks.NewMockHTTPClient(ctrl)
296	mockChrExec := mocks.NewMockChrootExecutor(ctrl)
297	mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
298	mockLogger := mocks.NewMockLogger(ctrl)
299
300	hooksDir := "/mnt/etc/pacman.d/hooks"
301	hookFile := hooksDir + "/limine-update.hook"
302	targetDisk := "/dev/sda"
303	hookTemplate := "Exec = /usr/bin/limine bios-install DISK_PLACEHOLDER"
304
305	mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
306	mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
307	mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
308	mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
309	mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
310	mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
311	mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte(hookTemplate)), nil).AnyTimes()
312	mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
313	mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
314	mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
315
316	hookWritten := false
317	mockFS.EXPECT().WriteFile(gomock.Eq(hookFile), gomock.Any(), gomock.Any()).DoAndReturn(
318		func(path string, data []byte, perm interface{}) error {
319			hookWritten = true
320			content := string(data)
321			if strings.Contains(content, "DISK_PLACEHOLDER") {
322				t.Errorf("hook content should not contain DISK_PLACEHOLDER, got: %s", content)
323			}
324			if !strings.Contains(content, targetDisk) {
325				t.Errorf("hook content should contain target disk %s, got: %s", targetDisk, content)
326			}
327			return nil
328		},
329	).AnyTimes()
330	mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
331
332	handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
333
334	cmd := commands.PostInstallCommand{
335		MountPoint:         "/mnt",
336		Username:           "testuser",
337		RunPostBootScripts: false,
338		PlymouthTheme:      "",
339		TargetDisk:         targetDisk,
340	}
341
342	result, err := handler.Handle(context.Background(), cmd)
343
344	if err != nil {
345		t.Fatalf("expected no error, got %v", err)
346	}
347
348	if !result.Success {
349		t.Error("expected success")
350	}
351
352	if !hookWritten {
353		t.Error("expected limine hook file to be written")
354	}
355}
356
357func TestPostInstallHandler_Handle_EnablesSnapperSync(t *testing.T) {
358	ctrl := gomock.NewController(t)
359	defer ctrl.Finish()
360
361	mockFS := mocks.NewMockFileSystem(ctrl)
362	mockHTTP := mocks.NewMockHTTPClient(ctrl)
363	mockChrExec := mocks.NewMockChrootExecutor(ctrl)
364	mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
365	mockLogger := mocks.NewMockLogger(ctrl)
366
367	pacmanInstalled := false
368	snapperTimerEnabled := false
369
370	mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
371	mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
372	mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
373	mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
374	mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
375	mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
376	mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
377	mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
378
379	// Track pacman install of limine-snapper-sync (specific expectation first)
380	mockChrExec.EXPECT().ExecuteInChroot(
381		gomock.Any(), gomock.Eq("/mnt"), gomock.Eq("pacman"),
382		gomock.Eq("-S"), gomock.Eq("--noconfirm"), gomock.Eq("--needed"), gomock.Eq("limine-snapper-sync"),
383	).DoAndReturn(func(ctx context.Context, mountPoint, command string, args ...string) ([]byte, error) {
384		pacmanInstalled = true
385		return []byte{}, nil
386	}).AnyTimes()
387	// Catch-all for any other ExecuteInChroot calls
388	mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
389
390	// Track systemctl enable for limine-snapper-sync.service (specific expectation first)
391	mockChrExec.EXPECT().ChrootSystemctl(
392		gomock.Any(), gomock.Any(), gomock.Eq("/mnt"), gomock.Eq("enable"), gomock.Eq("limine-snapper-sync.service"),
393	).DoAndReturn(func(ctx context.Context, logPath, chrootPath string, args ...string) error {
394		snapperTimerEnabled = true
395		return nil
396	}).AnyTimes()
397	// Catch-all for any other ChrootSystemctl calls
398	mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
399	mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
400
401	handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
402
403	cmd := commands.PostInstallCommand{
404		MountPoint:         "/mnt",
405		Username:           "testuser",
406		RunPostBootScripts: false,
407		PlymouthTheme:      "",
408	}
409
410	result, err := handler.Handle(context.Background(), cmd)
411
412	if err != nil {
413		t.Fatalf("expected no error, got %v", err)
414	}
415
416	if !result.Success {
417		t.Error("expected success")
418	}
419
420	if !pacmanInstalled {
421		t.Error("expected pacman to install limine-snapper-sync")
422	}
423
424	if !snapperTimerEnabled {
425		t.Error("expected limine-snapper-sync.service to be enabled via systemctl")
426	}
427}
428
429func TestPostInstallHandler_Handle_Everything(t *testing.T) {
430	ctrl := gomock.NewController(t)
431	defer ctrl.Finish()
432
433	mockFS := mocks.NewMockFileSystem(ctrl)
434	mockHTTP := mocks.NewMockHTTPClient(ctrl)
435	mockChrExec := mocks.NewMockChrootExecutor(ctrl)
436	mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
437	mockLogger := mocks.NewMockLogger(ctrl)
438
439	mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
440	mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
441	mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
442	mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
443	mockFS.EXPECT().Exists(gomock.Any()).Return(true, nil).AnyTimes()
444	mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("content"), nil).AnyTimes()
445	mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
446	mockFS.EXPECT().Chmod(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
447	mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
448	mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
449	mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
450	mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
451
452	handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
453
454	cmd := commands.PostInstallCommand{
455		MountPoint:         "/mnt",
456		Username:           "testuser",
457		RunPostBootScripts: true,
458		PlymouthTheme:      "bgrt",
459	}
460
461	result, err := handler.Handle(context.Background(), cmd)
462
463	if err != nil {
464		t.Fatalf("expected no error, got %v", err)
465	}
466
467	if !result.Success {
468		t.Error("expected success")
469	}
470
471	if len(result.TasksRun) != 2 {
472		t.Errorf("expected 2 tasks run, got %d", len(result.TasksRun))
473	}
474
475	// Check that both tasks are in the results
476	tasksFound := make(map[string]bool)
477	for _, task := range result.TasksRun {
478		tasksFound[task] = true
479	}
480
481	if !tasksFound["post-boot-scripts"] {
482		t.Error("expected post-boot-scripts task")
483	}
484
485	if !tasksFound["plymouth-theme-bgrt"] {
486		t.Error("expected plymouth-theme-bgrt task")
487	}
488}
489
490func TestPostInstallHandler_Handle_RunsVerification(t *testing.T) {
491	ctrl := gomock.NewController(t)
492	defer ctrl.Finish()
493
494	mockFS := mocks.NewMockFileSystem(ctrl)
495	mockHTTP := mocks.NewMockHTTPClient(ctrl)
496	mockChrExec := mocks.NewMockChrootExecutor(ctrl)
497	mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
498	mockLogger := mocks.NewMockLogger(ctrl)
499
500	mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
501	mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
502	mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
503	mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
504	mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
505	mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
506	mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
507	mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
508	mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
509	mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
510
511	// Stat returns nil (files exist) for the checked paths
512	mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
513
514	handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
515
516	cmd := commands.PostInstallCommand{
517		MountPoint:         "/mnt",
518		Username:           "testuser",
519		RunPostBootScripts: false,
520		PlymouthTheme:      "",
521		Encrypted:          false,
522	}
523
524	result, err := handler.Handle(context.Background(), cmd)
525
526	if err != nil {
527		t.Fatalf("expected no error, got %v", err)
528	}
529
530	if !result.Success {
531		t.Error("expected success")
532	}
533
534	if len(result.VerificationWarnings) != 0 {
535		t.Errorf("expected no verification warnings, got: %v", result.VerificationWarnings)
536	}
537}
538
539func TestPostInstallHandler_SanitizeLimineSnapperSyncConfig(t *testing.T) {
540	const mountPoint = "/mnt"
541	const confPath = "/mnt/etc/limine-snapper-sync.conf"
542	const enrollBin = "/mnt/usr/bin/limine-reset-enroll"
543	const sampleConfig = `# limine-snapper-sync config
544COMMANDS_BEFORE_SAVE="limine-reset-enroll"
545COMMANDS_AFTER_SAVE="limine-enroll-config"
546OTHER_OPTION="keep"
547`
548
549	tests := []struct {
550		name         string
551		confExists   bool
552		binStatErr   error
553		expectWrite  bool
554		checkContent func(t *testing.T, content string)
555	}{
556		{
557			name:        "binary missing config present — comments out enrollment lines",
558			confExists:  true,
559			binStatErr:  fmt.Errorf("no such file"),
560			expectWrite: true,
561			checkContent: func(t *testing.T, content string) {
562				t.Helper()
563				if strings.Contains(content, `COMMANDS_BEFORE_SAVE="limine-reset-enroll"`) {
564					t.Error("expected COMMANDS_BEFORE_SAVE to be commented out")
565				}
566				if strings.Contains(content, `COMMANDS_AFTER_SAVE="limine-enroll-config"`) {
567					t.Error("expected COMMANDS_AFTER_SAVE to be commented out")
568				}
569				if !strings.Contains(content, "# COMMANDS_BEFORE_SAVE=") {
570					t.Error("expected commented COMMANDS_BEFORE_SAVE line")
571				}
572				if !strings.Contains(content, "# COMMANDS_AFTER_SAVE=") {
573					t.Error("expected commented COMMANDS_AFTER_SAVE line")
574				}
575				if !strings.Contains(content, "disabled: limine-entry-tool not installed") {
576					t.Error("expected comment suffix 'disabled: limine-entry-tool not installed'")
577				}
578				if !strings.Contains(content, `OTHER_OPTION="keep"`) {
579					t.Error("expected OTHER_OPTION line to be preserved")
580				}
581			},
582		},
583		{
584			name:        "binary present — no modification",
585			confExists:  true,
586			binStatErr:  nil,
587			expectWrite: false,
588		},
589		{
590			name:        "config absent — no modification",
591			confExists:  false,
592			expectWrite: false,
593		},
594	}
595
596	for _, tc := range tests {
597		t.Run(tc.name, func(t *testing.T) {
598			ctrl := gomock.NewController(t)
599			defer ctrl.Finish()
600
601			mockFS := mocks.NewMockFileSystem(ctrl)
602			mockHTTP := mocks.NewMockHTTPClient(ctrl)
603			mockChrExec := mocks.NewMockChrootExecutor(ctrl)
604			mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
605			mockLogger := mocks.NewMockLogger(ctrl)
606
607			// conf exists check
608			mockFS.EXPECT().Exists(gomock.Eq(confPath)).Return(tc.confExists, nil)
609
610			if tc.confExists {
611				// binary stat check
612				mockFS.EXPECT().Stat(gomock.Eq(enrollBin)).Return(nil, tc.binStatErr)
613
614				if tc.binStatErr != nil {
615					// binary absent: expect ReadFile then WriteFile
616					mockFS.EXPECT().ReadFile(gomock.Eq(confPath)).Return([]byte(sampleConfig), nil)
617					if tc.expectWrite {
618						mockFS.EXPECT().WriteFile(gomock.Eq(confPath), gomock.Any(), gomock.Any()).DoAndReturn(
619							func(_ string, data []byte, _ interface{}) error {
620								if tc.checkContent != nil {
621									tc.checkContent(t, string(data))
622								}
623								return nil
624							},
625						)
626					}
627				}
628				// binary present: no ReadFile, no WriteFile
629			}
630			// config absent: no further calls
631
632			handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
633			err := handler.sanitizeLimineSnapperSyncConfig(mountPoint)
634			if err != nil {
635				t.Fatalf("expected no error, got %v", err)
636			}
637		})
638	}
639}
640
641func TestPostInstallHandler_Handle_VerificationWarnsOnMissingFiles(t *testing.T) {
642	ctrl := gomock.NewController(t)
643	defer ctrl.Finish()
644
645	mockFS := mocks.NewMockFileSystem(ctrl)
646	mockHTTP := mocks.NewMockHTTPClient(ctrl)
647	mockChrExec := mocks.NewMockChrootExecutor(ctrl)
648	mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
649	mockLogger := mocks.NewMockLogger(ctrl)
650
651	mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
652	mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
653	mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
654	mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
655	mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
656	mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
657	mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
658	mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
659	mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
660	mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
661
662	// limine.conf is missing; all other Stat calls succeed
663	mockFS.EXPECT().Stat(gomock.Eq("/mnt/boot/limine.conf")).Return(nil, fmt.Errorf("not found"))
664	mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
665
666	handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
667
668	cmd := commands.PostInstallCommand{
669		MountPoint:         "/mnt",
670		Username:           "testuser",
671		RunPostBootScripts: false,
672		PlymouthTheme:      "",
673		Encrypted:          false,
674	}
675
676	result, err := handler.Handle(context.Background(), cmd)
677
678	if err != nil {
679		t.Fatalf("expected no error, got %v", err)
680	}
681
682	if !result.Success {
683		t.Error("expected success even with verification warnings")
684	}
685
686	if len(result.VerificationWarnings) == 0 {
687		t.Error("expected verification warnings for missing limine.conf")
688	}
689
690	found := false
691	for _, w := range result.VerificationWarnings {
692		if strings.Contains(w, "limine.conf") {
693			found = true
694			break
695		}
696	}
697	if !found {
698		t.Errorf("expected warning about limine.conf, got: %v", result.VerificationWarnings)
699	}
700}

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