1package services
2
3import (
4 "context"
5 "net/http"
6 "testing"
7
8 "github.com/bnema/archup/internal/application/commands"
9 "github.com/bnema/archup/internal/application/handlers"
10 "github.com/bnema/archup/internal/domain/bootloader"
11 "github.com/bnema/archup/internal/domain/disk"
12 "github.com/bnema/archup/internal/domain/packages"
13 "github.com/bnema/archup/internal/domain/ports"
14 "github.com/bnema/archup/internal/domain/ports/mocks"
15 "go.uber.org/mock/gomock"
16)
17
18func newMockResponse(ctrl *gomock.Controller, statusCode int, body []byte) ports.Response {
19 resp := mocks.NewMockResponse(ctrl)
20 resp.EXPECT().StatusCode().Return(statusCode).AnyTimes()
21 resp.EXPECT().Body().Return(body).AnyTimes()
22 resp.EXPECT().Close().Return(nil).AnyTimes()
23 return resp
24}
25
26func createTestService(ctrl *gomock.Controller) *InstallationService {
27 mockFS := mocks.NewMockFileSystem(ctrl)
28 mockExec := mocks.NewMockCommandExecutor(ctrl)
29 mockChrExec := mocks.NewMockChrootExecutor(ctrl)
30 mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
31 mockHTTP := mocks.NewMockHTTPClient(ctrl)
32 mockRepo := mocks.NewMockInstallationRepository(ctrl)
33 mockLogger := mocks.NewMockLogger(ctrl)
34
35 mockLogger.EXPECT().Info(gomock.Any()).AnyTimes()
36 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
37 mockLogger.EXPECT().Warn(gomock.Any()).AnyTimes()
38 mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
39 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes()
40 mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
41 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
42 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
43 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
44 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
45 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
46 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
47 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
48 mockFS.EXPECT().Exists(gomock.Any()).Return(true, nil).AnyTimes()
49 mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("HOOKS=(base)\n"), nil).AnyTimes()
50 mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
51 mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
52 mockFS.EXPECT().Chmod(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
53 mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
54 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
55 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
56 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
57 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
58 mockChrExec.EXPECT().ExecuteInChrootWithStdin(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
59 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
60
61 bootstrapHandler := handlers.NewBootstrapHandler(mockFS, mockHTTP, mockLogger, "https://github.com/bnema/archup", "https://raw.githubusercontent.com/bnema/archup/dev", "dev")
62 preflightHandler := handlers.NewPreflightHandler(mockFS, mockExec, mockLogger)
63 partitionHandler := handlers.NewPartitionHandler(mockExec, mockLogger)
64 baseHandler := handlers.NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger)
65 configHandler := handlers.NewConfigureSystemHandler(mockFS, mockChrExec, mockLogger)
66 bootloaderHandler := handlers.NewBootloaderHandler(mockFS, mockExec, mockChrExec, mockLogger)
67 reposHandler := handlers.NewReposHandler(mockFS, mockChrExec, mockLogger)
68 postInstallHandler := handlers.NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
69
70 return NewInstallationService(
71 mockRepo,
72 mockLogger,
73 bootstrapHandler,
74 preflightHandler,
75 partitionHandler,
76 baseHandler,
77 configHandler,
78 bootloaderHandler,
79 reposHandler,
80 postInstallHandler,
81 )
82}
83
84func TestInstallationService_Start(t *testing.T) {
85 ctrl := gomock.NewController(t)
86 defer ctrl.Finish()
87
88 service := createTestService(ctrl)
89 defer func() {
90 if err := service.Close(); err != nil {
91 t.Fatalf("expected no error, got %v", err)
92 }
93 }()
94
95 ctx := context.Background()
96 err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none")
97
98 if err != nil {
99 t.Fatalf("expected no error, got %v", err)
100 }
101
102 if service.installAgg == nil {
103 t.Fatal("expected installation to be created")
104 }
105
106 if service.installAgg.Hostname() != "myarch" {
107 t.Errorf("expected hostname myarch, got %s", service.installAgg.Hostname())
108 }
109}
110
111func TestInstallationService_Start_InvalidHostname(t *testing.T) {
112 ctrl := gomock.NewController(t)
113 defer ctrl.Finish()
114
115 mockFS := mocks.NewMockFileSystem(ctrl)
116 mockExec := mocks.NewMockCommandExecutor(ctrl)
117 mockChrExec := mocks.NewMockChrootExecutor(ctrl)
118 mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
119 mockHTTP := mocks.NewMockHTTPClient(ctrl)
120 mockRepo := mocks.NewMockInstallationRepository(ctrl)
121 mockLogger := mocks.NewMockLogger(ctrl)
122
123 mockLogger.EXPECT().Info(gomock.Any()).AnyTimes()
124 mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
125 mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
126 mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
127 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
128 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
129 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
130 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
131 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
132 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
133 mockExec.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
134 mockFS.EXPECT().Exists(gomock.Any()).Return(true, nil).AnyTimes()
135 mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("HOOKS=(base)\n"), nil).AnyTimes()
136 mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
137 mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
138 mockFS.EXPECT().Chmod(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
139 mockFS.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
140 mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
141 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
142 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
143 mockChrExec.EXPECT().ExecuteInChroot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte{}, nil).AnyTimes()
144 mockChrExec.EXPECT().ExecuteInChrootWithStdin(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
145 mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
146
147 bootstrapHandler := handlers.NewBootstrapHandler(mockFS, mockHTTP, mockLogger, "https://github.com/bnema/archup", "https://raw.githubusercontent.com/bnema/archup/dev", "dev")
148 preflightHandler := handlers.NewPreflightHandler(mockFS, mockExec, mockLogger)
149 partitionHandler := handlers.NewPartitionHandler(mockExec, mockLogger)
150 baseHandler := handlers.NewInstallBaseHandler(mockFS, mockExec, mockChrExec, mockLogger)
151 configHandler := handlers.NewConfigureSystemHandler(mockFS, mockChrExec, mockLogger)
152 bootloaderHandler := handlers.NewBootloaderHandler(mockFS, mockExec, mockChrExec, mockLogger)
153 reposHandler := handlers.NewReposHandler(mockFS, mockChrExec, mockLogger)
154 postInstallHandler := handlers.NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
155
156 service := NewInstallationService(mockRepo, mockLogger, bootstrapHandler, preflightHandler, partitionHandler, baseHandler, configHandler, bootloaderHandler, reposHandler, postInstallHandler)
157 defer func() {
158 if err := service.Close(); err != nil {
159 t.Fatalf("expected no error, got %v", err)
160 }
161 }()
162
163 ctx := context.Background()
164 err := service.Start(ctx, "", "testuser", "/dev/sda", "none")
165
166 if err == nil {
167 t.Error("expected error for invalid hostname")
168 }
169
170 if service.installAgg != nil {
171 t.Error("expected installation not to be created")
172 }
173}
174
175func TestInstallationService_GetStatus_NotStarted(t *testing.T) {
176 ctrl := gomock.NewController(t)
177 defer ctrl.Finish()
178
179 service := createTestService(ctrl)
180 defer func() {
181 if err := service.Close(); err != nil {
182 t.Fatalf("expected no error, got %v", err)
183 }
184 }()
185
186 status := service.GetStatus()
187
188 if status.State != "NotStarted" {
189 t.Errorf("expected state NotStarted, got %s", status.State)
190 }
191}
192
193func TestInstallationService_GetStatus_Started(t *testing.T) {
194 ctrl := gomock.NewController(t)
195 defer ctrl.Finish()
196
197 service := createTestService(ctrl)
198 defer func() {
199 if err := service.Close(); err != nil {
200 t.Fatalf("expected no error, got %v", err)
201 }
202 }()
203
204 ctx := context.Background()
205 if err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none"); err != nil {
206 t.Fatalf("expected no error, got %v", err)
207 }
208
209 status := service.GetStatus()
210
211 if status.State != "PreflightChecks" {
212 t.Errorf("expected state PreflightChecks, got %s", status.State)
213 }
214
215 if status.Hostname != "myarch" {
216 t.Errorf("expected hostname myarch, got %s", status.Hostname)
217 }
218
219 if status.Username != "testuser" {
220 t.Errorf("expected username testuser, got %s", status.Username)
221 }
222}
223
224func TestInstallationService_RunPreflight_NotRoot(t *testing.T) {
225 ctrl := gomock.NewController(t)
226 defer ctrl.Finish()
227
228 service := createTestService(ctrl)
229 defer func() {
230 if err := service.Close(); err != nil {
231 t.Fatalf("expected no error, got %v", err)
232 }
233 }()
234
235 ctx := context.Background()
236 if err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none"); err != nil {
237 t.Fatalf("expected no error, got %v", err)
238 }
239
240 // We're probably not running as root, so preflight will fail
241 result, _ := service.RunPreflight(ctx)
242
243 // Either expect error or result indicating failure
244 if result != nil && result.ChecksPassed {
245 // If we're running as root, both err and checksPassed should be false
246 t.Error("expected preflight to fail when not running as root")
247 }
248}
249
250func TestInstallationService_RunPartition(t *testing.T) {
251 ctrl := gomock.NewController(t)
252 defer ctrl.Finish()
253
254 service := createTestService(ctrl)
255 defer func() {
256 if err := service.Close(); err != nil {
257 t.Fatalf("expected no error, got %v", err)
258 }
259 }()
260
261 ctx := context.Background()
262 if err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none"); err != nil {
263 t.Fatalf("expected no error, got %v", err)
264 }
265
266 cmd := commands.PartitionDiskCommand{
267 TargetDisk: "/dev/sda",
268 RootSizeGB: 50,
269 BootSizeGB: 1,
270 EncryptionType: disk.EncryptionTypeNone,
271 FilesystemType: disk.FilesystemExt4,
272 }
273
274 result, err := service.RunPartition(ctx, cmd)
275
276 if err != nil {
277 t.Fatalf("expected no error, got %v", err)
278 }
279
280 if !result.Success {
281 t.Errorf("expected success, got error: %s", result.ErrorDetail)
282 }
283}
284
285func TestInstallationService_RunBaseInstall(t *testing.T) {
286 ctrl := gomock.NewController(t)
287 defer ctrl.Finish()
288
289 service := createTestService(ctrl)
290 defer func() {
291 if err := service.Close(); err != nil {
292 t.Fatalf("expected no error, got %v", err)
293 }
294 }()
295
296 ctx := context.Background()
297 if err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none"); err != nil {
298 t.Fatalf("expected no error, got %v", err)
299 }
300
301 cmd := commands.InstallBaseCommand{
302 TargetDisk: "/dev/sda",
303 MountPoint: "/mnt",
304 Packages: []string{},
305 KernelVariant: packages.KernelStable,
306 IncludeMicrocode: false,
307 }
308
309 result, err := service.RunBaseInstall(ctx, cmd)
310
311 if err != nil {
312 t.Fatalf("expected no error, got %v", err)
313 }
314
315 if !result.Success {
316 t.Error("expected success")
317 }
318}
319
320func TestInstallationService_RunConfigSystem(t *testing.T) {
321 ctrl := gomock.NewController(t)
322 defer ctrl.Finish()
323
324 service := createTestService(ctrl)
325 defer func() {
326 if err := service.Close(); err != nil {
327 t.Fatalf("expected no error, got %v", err)
328 }
329 }()
330
331 ctx := context.Background()
332 if err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none"); err != nil {
333 t.Fatalf("expected no error, got %v", err)
334 }
335
336 cmd := commands.ConfigureSystemCommand{
337 MountPoint: "/mnt",
338 Hostname: "myarch",
339 Timezone: "UTC",
340 Locale: "en_US.UTF-8",
341 Keymap: "us",
342 Username: "testuser",
343 UserShell: "/bin/bash",
344 UserPassword: "testpass123",
345 RootPassword: "rootpass456",
346 }
347
348 result, err := service.RunConfigSystem(ctx, cmd)
349
350 if err != nil {
351 t.Fatalf("expected no error, got %v", err)
352 }
353
354 if !result.Success {
355 t.Error("expected success")
356 }
357}
358
359func TestInstallationService_RunBootloaderSetup(t *testing.T) {
360 ctrl := gomock.NewController(t)
361 defer ctrl.Finish()
362
363 service := createTestService(ctrl)
364 defer func() {
365 if err := service.Close(); err != nil {
366 t.Fatalf("expected no error, got %v", err)
367 }
368 }()
369
370 ctx := context.Background()
371 if err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none"); err != nil {
372 t.Fatalf("expected no error, got %v", err)
373 }
374
375 cmd := commands.InstallBootloaderCommand{
376 MountPoint: "/mnt",
377 BootloaderType: bootloader.BootloaderTypeLimine,
378 TimeoutSeconds: 5,
379 Branding: "ArchUp",
380 KernelVariant: packages.KernelStable,
381 RootPartition: "/dev/sda2",
382 EncryptionType: disk.EncryptionTypeNone,
383 EFIPartition: "/dev/sda1",
384 TargetDisk: "/dev/sda",
385 }
386
387 result, err := service.RunBootloaderSetup(ctx, cmd)
388
389 if err != nil {
390 t.Fatalf("expected no error, got %v", err)
391 }
392
393 if !result.Success {
394 t.Error("expected success")
395 }
396}
397
398func TestInstallationService_RunRepositorySetup(t *testing.T) {
399 ctrl := gomock.NewController(t)
400 defer ctrl.Finish()
401
402 service := createTestService(ctrl)
403 defer func() {
404 if err := service.Close(); err != nil {
405 t.Fatalf("expected no error, got %v", err)
406 }
407 }()
408
409 ctx := context.Background()
410 if err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none"); err != nil {
411 t.Fatalf("expected no error, got %v", err)
412 }
413
414 cmd := commands.SetupRepositoriesCommand{
415 MountPoint: "/mnt",
416 EnableMultilib: false,
417 AURHelper: packages.AURHelperParu,
418 AdditionalRepos: []string{},
419 }
420
421 result, err := service.RunRepositorySetup(ctx, cmd)
422
423 if err != nil {
424 t.Fatalf("expected no error, got %v", err)
425 }
426
427 if !result.Success {
428 t.Error("expected success")
429 }
430}
431
432func TestInstallationService_RunPostInstall(t *testing.T) {
433 ctrl := gomock.NewController(t)
434 defer ctrl.Finish()
435
436 service := createTestService(ctrl)
437 defer func() {
438 if err := service.Close(); err != nil {
439 t.Fatalf("expected no error, got %v", err)
440 }
441 }()
442
443 ctx := context.Background()
444 if err := service.Start(ctx, "myarch", "testuser", "/dev/sda", "none"); err != nil {
445 t.Fatalf("expected no error, got %v", err)
446 }
447
448 cmd := commands.PostInstallCommand{
449 MountPoint: "/mnt",
450 Username: "testuser",
451 RunPostBootScripts: false,
452 PlymouthTheme: "",
453 }
454
455 result, err := service.RunPostInstall(ctx, cmd)
456
457 if err != nil {
458 t.Fatalf("expected no error, got %v", err)
459 }
460
461 if !result.Success {
462 t.Error("expected success")
463 }
464}
465
466func TestInstallationService_CompleteNotStarted(t *testing.T) {
467 ctrl := gomock.NewController(t)
468 defer ctrl.Finish()
469
470 service := createTestService(ctrl)
471 defer func() {
472 if err := service.Close(); err != nil {
473 t.Fatalf("expected no error, got %v", err)
474 }
475 }()
476
477 ctx := context.Background()
478 // Don't start installation, try to complete it
479
480 err := service.Complete(ctx)
481
482 if err == nil {
483 t.Error("expected error when completing unstarted installation")
484 }
485}
486
487func TestInstallationService_SubscribeProgress(t *testing.T) {
488 ctrl := gomock.NewController(t)
489 defer ctrl.Finish()
490
491 service := createTestService(ctrl)
492 defer func() {
493 if err := service.Close(); err != nil {
494 t.Fatalf("expected no error, got %v", err)
495 }
496 }()
497
498 ch := service.SubscribeProgress()
499
500 if ch == nil {
501 t.Fatal("expected non-nil progress channel")
502 }
503}