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}