detached ed51bf5

test(phases): add comprehensive unit tests for config phase

commit
ed51bf509203d9884d2772ac898cd1a221918522
author
bnema · · 2025-10-27 18:25 UTC
committer
bnema · · 2025-10-27 22:29 UTC
parent
1357af0
signature
unsigned
1 file · +326 -0
A internal/phases/systemconfig_test.go +326 -0
@@ -0,0 +1,326 @@
1+package phases
2+
3+import (
4+ "fmt"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/archup/internal/config"
11+ "github.com/bnema/archup/internal/interfaces/mocks"
12+ "github.com/bnema/archup/internal/logger"
13+ "github.com/bnema/archup/internal/system"
14+ "go.uber.org/mock/gomock"
15+)
16+
17+// TestConfigPhasePreCheck tests configuration prerequisites
18+func TestConfigPhasePreCheck(t *testing.T) {
19+ tests := []struct {
20+ name string
21+ hostname string
22+ username string
23+ userPass string
24+ timezone string
25+ mountResult system.CommandResult
26+ wantErr bool
27+ errContains string
28+ }{
29+ {
30+ name: "All required fields present - /mnt mounted",
31+ hostname: "archup-machine",
32+ username: "archuser",
33+ userPass: "password123",
34+ timezone: "UTC",
35+ mountResult: system.CommandResult{ExitCode: 0},
36+ wantErr: false,
37+ },
38+ {
39+ name: "Missing hostname",
40+ hostname: "",
41+ username: "archuser",
42+ userPass: "password123",
43+ timezone: "UTC",
44+ mountResult: system.CommandResult{ExitCode: 0},
45+ wantErr: true,
46+ errContains: "hostname is required",
47+ },
48+ {
49+ name: "Missing username",
50+ hostname: "archup-machine",
51+ username: "",
52+ userPass: "password123",
53+ timezone: "UTC",
54+ mountResult: system.CommandResult{ExitCode: 0},
55+ wantErr: true,
56+ errContains: "username is required",
57+ },
58+ {
59+ name: "Missing user password",
60+ hostname: "archup-machine",
61+ username: "archuser",
62+ userPass: "",
63+ timezone: "UTC",
64+ mountResult: system.CommandResult{ExitCode: 0},
65+ wantErr: true,
66+ errContains: "user password is required",
67+ },
68+ {
69+ name: "Missing timezone",
70+ hostname: "archup-machine",
71+ username: "archuser",
72+ userPass: "password123",
73+ timezone: "",
74+ mountResult: system.CommandResult{ExitCode: 0},
75+ wantErr: true,
76+ errContains: "timezone is required",
77+ },
78+ {
79+ name: "/mnt not mounted",
80+ hostname: "archup-machine",
81+ username: "archuser",
82+ userPass: "password123",
83+ timezone: "UTC",
84+ mountResult: system.CommandResult{ExitCode: 32},
85+ wantErr: true,
86+ errContains: "is not mounted",
87+ },
88+ }
89+
90+ for _, tt := range tests {
91+ t.Run(tt.name, func(t *testing.T) {
92+ ctrl := gomock.NewController(t)
93+ defer ctrl.Finish()
94+
95+ mockFS := mocks.NewMockFileSystem(ctrl)
96+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
97+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
98+
99+ mockSysExec.EXPECT().RunSimple("mountpoint", "-q", config.PathMnt).Return(tt.mountResult).Times(1)
100+
101+ tmpDir := t.TempDir()
102+ logPath := filepath.Join(tmpDir, "test.log")
103+ log, err := logger.New(logPath, false)
104+ if err != nil {
105+ t.Fatalf("Failed to create logger: %v", err)
106+ }
107+ defer log.Close()
108+
109+ cfg := config.NewConfig("test")
110+ cfg.Hostname = tt.hostname
111+ cfg.Username = tt.username
112+ cfg.UserPassword = tt.userPass
113+ cfg.Timezone = tt.timezone
114+
115+ phase := NewConfigPhase(cfg, log, mockFS, mockSysExec, mockChrExec)
116+
117+ err = phase.PreCheck()
118+ if (err != nil) != tt.wantErr {
119+ t.Errorf("PreCheck() error = %v, wantErr %v", err, tt.wantErr)
120+ return
121+ }
122+ if tt.wantErr && err != nil {
123+ if errMsg := err.Error(); tt.errContains != "" && !containsSubstring(errMsg, tt.errContains) {
124+ t.Errorf("PreCheck() error = %q, should contain %q", errMsg, tt.errContains)
125+ }
126+ }
127+ })
128+ }
129+}
130+
131+// TestConfigPhasePostCheck tests configuration validation
132+func TestConfigPhasePostCheck(t *testing.T) {
133+ tests := []struct {
134+ name string
135+ setupMocks func(*mocks.MockFileSystem)
136+ wantErr bool
137+ errContains string
138+ }{
139+ {
140+ name: "All configuration files exist - postcheck passes",
141+ setupMocks: func(mockFS *mocks.MockFileSystem) {
142+ mockFS.EXPECT().Stat(config.PathMntEtcHostname).Return(mockFileInfo{}, nil).Times(1)
143+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(false).Times(1)
144+ mockFS.EXPECT().Stat(config.PathMntEtcLocaleConf).Return(mockFileInfo{}, nil).Times(1)
145+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(false).Times(1)
146+ zramPath := filepath.Join(config.PathMntEtcSystemd, config.FileZramGenerator)
147+ mockFS.EXPECT().Stat(zramPath).Return(mockFileInfo{}, nil).Times(1)
148+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(false).Times(1)
149+ },
150+ wantErr: false,
151+ },
152+ {
153+ name: "Hostname file missing - postcheck fails",
154+ setupMocks: func(mockFS *mocks.MockFileSystem) {
155+ mockFS.EXPECT().Stat(config.PathMntEtcHostname).Return(nil, fmt.Errorf("not found")).Times(1)
156+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(true).Times(1)
157+ },
158+ wantErr: true,
159+ errContains: "hostname file was not created",
160+ },
161+ {
162+ name: "Locale.conf file missing - postcheck fails",
163+ setupMocks: func(mockFS *mocks.MockFileSystem) {
164+ mockFS.EXPECT().Stat(config.PathMntEtcHostname).Return(mockFileInfo{}, nil).Times(1)
165+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(false).Times(1)
166+ mockFS.EXPECT().Stat(config.PathMntEtcLocaleConf).Return(nil, fmt.Errorf("not found")).Times(1)
167+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(true).Times(1)
168+ },
169+ wantErr: true,
170+ errContains: "locale.conf was not created",
171+ },
172+ }
173+
174+ for _, tt := range tests {
175+ t.Run(tt.name, func(t *testing.T) {
176+ ctrl := gomock.NewController(t)
177+ defer ctrl.Finish()
178+
179+ mockFS := mocks.NewMockFileSystem(ctrl)
180+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
181+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
182+
183+ tt.setupMocks(mockFS)
184+
185+ tmpDir := t.TempDir()
186+ logPath := filepath.Join(tmpDir, "test.log")
187+ log, err := logger.New(logPath, false)
188+ if err != nil {
189+ t.Fatalf("Failed to create logger: %v", err)
190+ }
191+ defer log.Close()
192+
193+ cfg := config.NewConfig("test")
194+ cfg.ConfigPath = filepath.Join(tmpDir, "config")
195+
196+ phase := NewConfigPhase(cfg, log, mockFS, mockSysExec, mockChrExec)
197+
198+ err = phase.PostCheck()
199+ if (err != nil) != tt.wantErr {
200+ t.Errorf("PostCheck() error = %v, wantErr %v", err, tt.wantErr)
201+ return
202+ }
203+ if tt.wantErr && err != nil {
204+ if errMsg := err.Error(); tt.errContains != "" && !containsSubstring(errMsg, tt.errContains) {
205+ t.Errorf("PostCheck() error = %q, should contain %q", errMsg, tt.errContains)
206+ }
207+ }
208+ })
209+ }
210+}
211+
212+// TestConfigPhaseExecute tests the Execute method
213+func TestConfigPhaseExecute(t *testing.T) {
214+ ctrl := gomock.NewController(t)
215+ defer ctrl.Finish()
216+
217+ mockFS := mocks.NewMockFileSystem(ctrl)
218+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
219+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
220+
221+ // Setup successful calls for all configuration steps
222+ mockChrExec.EXPECT().ChrootExec(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
223+ mockChrExec.EXPECT().ChrootExecWithStdin(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
224+ mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
225+ mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
226+
227+ tmpDir := t.TempDir()
228+ logPath := filepath.Join(tmpDir, "test.log")
229+ log, err := logger.New(logPath, false)
230+ if err != nil {
231+ t.Fatalf("Failed to create logger: %v", err)
232+ }
233+ defer log.Close()
234+
235+ cfg := config.NewConfig("test")
236+ cfg.Hostname = "archup"
237+ cfg.Username = "archuser"
238+ cfg.UserPassword = "password"
239+ cfg.Timezone = "UTC"
240+ cfg.Locale = "en_US"
241+ cfg.Keymap = "us"
242+
243+ phase := NewConfigPhase(cfg, log, mockFS, mockSysExec, mockChrExec)
244+
245+ progressChan := make(chan ProgressUpdate, 100)
246+ result := phase.Execute(progressChan)
247+
248+ if !result.Success {
249+ t.Errorf("Execute() Success = %v, want true", result.Success)
250+ }
251+ if result.Message != "Configuration complete" {
252+ t.Errorf("Execute() Message = %q, want 'Configuration complete'", result.Message)
253+ }
254+}
255+
256+// TestConfigPhaseRollback tests the Rollback method
257+func TestConfigPhaseRollback(t *testing.T) {
258+ ctrl := gomock.NewController(t)
259+ defer ctrl.Finish()
260+
261+ mockFS := mocks.NewMockFileSystem(ctrl)
262+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
263+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
264+
265+ tmpDir := t.TempDir()
266+ logPath := filepath.Join(tmpDir, "test.log")
267+ log, err := logger.New(logPath, false)
268+ if err != nil {
269+ t.Fatalf("Failed to create logger: %v", err)
270+ }
271+ defer log.Close()
272+
273+ cfg := config.NewConfig("test")
274+ phase := NewConfigPhase(cfg, log, mockFS, mockSysExec, mockChrExec)
275+
276+ // ConfigPhase Rollback should be no-op
277+ err = phase.Rollback()
278+ if err != nil {
279+ t.Errorf("Rollback() error = %v, want nil", err)
280+ }
281+}
282+
283+// TestConfigPhaseCanSkip tests the CanSkip method
284+func TestConfigPhaseCanSkip(t *testing.T) {
285+ ctrl := gomock.NewController(t)
286+ defer ctrl.Finish()
287+
288+ mockFS := mocks.NewMockFileSystem(ctrl)
289+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
290+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
291+
292+ tmpDir := t.TempDir()
293+ logPath := filepath.Join(tmpDir, "test.log")
294+ log, err := logger.New(logPath, false)
295+ if err != nil {
296+ t.Fatalf("Failed to create logger: %v", err)
297+ }
298+ defer log.Close()
299+
300+ cfg := config.NewConfig("test")
301+ phase := NewConfigPhase(cfg, log, mockFS, mockSysExec, mockChrExec)
302+
303+ if phase.CanSkip() {
304+ t.Error("CanSkip() = true, want false")
305+ }
306+}
307+
308+// Helper function to check if a string contains a substring
309+func containsSubstring(s, substr string) bool {
310+ for i := 0; i < len(s)-len(substr)+1; i++ {
311+ if s[i:i+len(substr)] == substr {
312+ return true
313+ }
314+ }
315+ return false
316+}
317+
318+// mockFileInfo implements os.FileInfo for testing
319+type mockFileInfo struct{}
320+
321+func (m mockFileInfo) Name() string { return "mock" }
322+func (m mockFileInfo) Size() int64 { return 0 }
323+func (m mockFileInfo) Mode() os.FileMode { return 0 }
324+func (m mockFileInfo) ModTime() time.Time { return time.Now() }
325+func (m mockFileInfo) IsDir() bool { return false }
326+func (m mockFileInfo) Sys() interface{} { return nil }
@@ -0,0 +1,326 @@
1+package phases
2+
3+import (
4+ "fmt"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/archup/internal/config"
11+ "github.com/bnema/archup/internal/interfaces/mocks"
12+ "github.com/bnema/archup/internal/logger"
13+ "github.com/bnema/archup/internal/system"
14+ "go.uber.org/mock/gomock"
15+)
16+
17+// TestConfigPhasePreCheck tests configuration prerequisites
18+func TestConfigPhasePreCheck(t *testing.T) {
19+ tests := []struct {
20+ name string
21+ hostname string
22+ username string
23+ userPass string
24+ timezone string
25+ mountResult system.CommandResult
26+ wantErr bool
27+ errContains string
28+ }{
29+ {
30+ name: "All required fields present - /mnt mounted",
31+ hostname: "archup-machine",
32+ username: "archuser",
33+ userPass: "password123",
34+ timezone: "UTC",
35+ mountResult: system.CommandResult{ExitCode: 0},
36+ wantErr: false,
37+ },
38+ {
39+ name: "Missing hostname",
40+ hostname: "",
41+ username: "archuser",
42+ userPass: "password123",
43+ timezone: "UTC",
44+ mountResult: system.CommandResult{ExitCode: 0},
45+ wantErr: true,
46+ errContains: "hostname is required",
47+ },
48+ {
49+ name: "Missing username",
50+ hostname: "archup-machine",
51+ username: "",
52+ userPass: "password123",
53+ timezone: "UTC",
54+ mountResult: system.CommandResult{ExitCode: 0},
55+ wantErr: true,
56+ errContains: "username is required",
57+ },
58+ {
59+ name: "Missing user password",
60+ hostname: "archup-machine",
61+ username: "archuser",
62+ userPass: "",
63+ timezone: "UTC",
64+ mountResult: system.CommandResult{ExitCode: 0},
65+ wantErr: true,
66+ errContains: "user password is required",
67+ },
68+ {
69+ name: "Missing timezone",
70+ hostname: "archup-machine",
71+ username: "archuser",
72+ userPass: "password123",
73+ timezone: "",
74+ mountResult: system.CommandResult{ExitCode: 0},
75+ wantErr: true,
76+ errContains: "timezone is required",
77+ },
78+ {
79+ name: "/mnt not mounted",
80+ hostname: "archup-machine",
81+ username: "archuser",
82+ userPass: "password123",
83+ timezone: "UTC",
84+ mountResult: system.CommandResult{ExitCode: 32},
85+ wantErr: true,
86+ errContains: "is not mounted",
87+ },
88+ }
89+
90+ for _, tt := range tests {
91+ t.Run(tt.name, func(t *testing.T) {
92+ ctrl := gomock.NewController(t)
93+ defer ctrl.Finish()
94+
95+ mockFS := mocks.NewMockFileSystem(ctrl)
96+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
97+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
98+
99+ mockSysExec.EXPECT().RunSimple("mountpoint", "-q", config.PathMnt).Return(tt.mountResult).Times(1)
100+
101+ tmpDir := t.TempDir()
102+ logPath := filepath.Join(tmpDir, "test.log")
103+ log, err := logger.New(logPath, false)
104+ if err != nil {
105+ t.Fatalf("Failed to create logger: %v", err)
106+ }
107+ defer log.Close()
108+
109+ cfg := config.NewConfig("test")
110+ cfg.Hostname = tt.hostname
111+ cfg.Username = tt.username
112+ cfg.UserPassword = tt.userPass
113+ cfg.Timezone = tt.timezone
114+
115+ phase := NewConfigPhase(cfg, log, mockFS, mockSysExec, mockChrExec)
116+
117+ err = phase.PreCheck()
118+ if (err != nil) != tt.wantErr {
119+ t.Errorf("PreCheck() error = %v, wantErr %v", err, tt.wantErr)
120+ return
121+ }
122+ if tt.wantErr && err != nil {
123+ if errMsg := err.Error(); tt.errContains != "" && !containsSubstring(errMsg, tt.errContains) {
124+ t.Errorf("PreCheck() error = %q, should contain %q", errMsg, tt.errContains)
125+ }
126+ }
127+ })
128+ }
129+}
130+
131+// TestConfigPhasePostCheck tests configuration validation
132+func TestConfigPhasePostCheck(t *testing.T) {
133+ tests := []struct {
134+ name string
135+ setupMocks func(*mocks.MockFileSystem)
136+ wantErr bool
137+ errContains string
138+ }{
139+ {
140+ name: "All configuration files exist - postcheck passes",
141+ setupMocks: func(mockFS *mocks.MockFileSystem) {
142+ mockFS.EXPECT().Stat(config.PathMntEtcHostname).Return(mockFileInfo{}, nil).Times(1)
143+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(false).Times(1)
144+ mockFS.EXPECT().Stat(config.PathMntEtcLocaleConf).Return(mockFileInfo{}, nil).Times(1)
145+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(false).Times(1)
146+ zramPath := filepath.Join(config.PathMntEtcSystemd, config.FileZramGenerator)
147+ mockFS.EXPECT().Stat(zramPath).Return(mockFileInfo{}, nil).Times(1)
148+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(false).Times(1)
149+ },
150+ wantErr: false,
151+ },
152+ {
153+ name: "Hostname file missing - postcheck fails",
154+ setupMocks: func(mockFS *mocks.MockFileSystem) {
155+ mockFS.EXPECT().Stat(config.PathMntEtcHostname).Return(nil, fmt.Errorf("not found")).Times(1)
156+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(true).Times(1)
157+ },
158+ wantErr: true,
159+ errContains: "hostname file was not created",
160+ },
161+ {
162+ name: "Locale.conf file missing - postcheck fails",
163+ setupMocks: func(mockFS *mocks.MockFileSystem) {
164+ mockFS.EXPECT().Stat(config.PathMntEtcHostname).Return(mockFileInfo{}, nil).Times(1)
165+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(false).Times(1)
166+ mockFS.EXPECT().Stat(config.PathMntEtcLocaleConf).Return(nil, fmt.Errorf("not found")).Times(1)
167+ mockFS.EXPECT().IsNotExist(gomock.Any()).Return(true).Times(1)
168+ },
169+ wantErr: true,
170+ errContains: "locale.conf was not created",
171+ },
172+ }
173+
174+ for _, tt := range tests {
175+ t.Run(tt.name, func(t *testing.T) {
176+ ctrl := gomock.NewController(t)
177+ defer ctrl.Finish()
178+
179+ mockFS := mocks.NewMockFileSystem(ctrl)
180+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
181+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
182+
183+ tt.setupMocks(mockFS)
184+
185+ tmpDir := t.TempDir()
186+ logPath := filepath.Join(tmpDir, "test.log")
187+ log, err := logger.New(logPath, false)
188+ if err != nil {
189+ t.Fatalf("Failed to create logger: %v", err)
190+ }
191+ defer log.Close()
192+
193+ cfg := config.NewConfig("test")
194+ cfg.ConfigPath = filepath.Join(tmpDir, "config")
195+
196+ phase := NewConfigPhase(cfg, log, mockFS, mockSysExec, mockChrExec)
197+
198+ err = phase.PostCheck()
199+ if (err != nil) != tt.wantErr {
200+ t.Errorf("PostCheck() error = %v, wantErr %v", err, tt.wantErr)
201+ return
202+ }
203+ if tt.wantErr && err != nil {
204+ if errMsg := err.Error(); tt.errContains != "" && !containsSubstring(errMsg, tt.errContains) {
205+ t.Errorf("PostCheck() error = %q, should contain %q", errMsg, tt.errContains)
206+ }
207+ }
208+ })
209+ }
210+}
211+
212+// TestConfigPhaseExecute tests the Execute method
213+func TestConfigPhaseExecute(t *testing.T) {
214+ ctrl := gomock.NewController(t)
215+ defer ctrl.Finish()
216+
217+ mockFS := mocks.NewMockFileSystem(ctrl)
218+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
219+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
220+
221+ // Setup successful calls for all configuration steps
222+ mockChrExec.EXPECT().ChrootExec(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
223+ mockChrExec.EXPECT().ChrootExecWithStdin(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
224+ mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
225+ mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
226+
227+ tmpDir := t.TempDir()
228+ logPath := filepath.Join(tmpDir, "test.log")
229+ log, err := logger.New(logPath, false)
230+ if err != nil {
231+ t.Fatalf("Failed to create logger: %v", err)
232+ }
233+ defer log.Close()
234+
235+ cfg := config.NewConfig("test")
236+ cfg.Hostname = "archup"
237+ cfg.Username = "archuser"
238+ cfg.UserPassword = "password"
239+ cfg.Timezone = "UTC"
240+ cfg.Locale = "en_US"
241+ cfg.Keymap = "us"
242+
243+ phase := NewConfigPhase(cfg, log, mockFS, mockSysExec, mockChrExec)
244+
245+ progressChan := make(chan ProgressUpdate, 100)
246+ result := phase.Execute(progressChan)
247+
248+ if !result.Success {
249+ t.Errorf("Execute() Success = %v, want true", result.Success)
250+ }
251+ if result.Message != "Configuration complete" {
252+ t.Errorf("Execute() Message = %q, want 'Configuration complete'", result.Message)
253+ }
254+}
255+
256+// TestConfigPhaseRollback tests the Rollback method
257+func TestConfigPhaseRollback(t *testing.T) {
258+ ctrl := gomock.NewController(t)
259+ defer ctrl.Finish()
260+
261+ mockFS := mocks.NewMockFileSystem(ctrl)
262+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
263+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
264+
265+ tmpDir := t.TempDir()
266+ logPath := filepath.Join(tmpDir, "test.log")
267+ log, err := logger.New(logPath, false)
268+ if err != nil {
269+ t.Fatalf("Failed to create logger: %v", err)
270+ }
271+ defer log.Close()
272+
273+ cfg := config.NewConfig("test")
274+ phase := NewConfigPhase(cfg, log, mockFS, mockSysExec, mockChrExec)
275+
276+ // ConfigPhase Rollback should be no-op
277+ err = phase.Rollback()
278+ if err != nil {
279+ t.Errorf("Rollback() error = %v, want nil", err)
280+ }
281+}
282+
283+// TestConfigPhaseCanSkip tests the CanSkip method
284+func TestConfigPhaseCanSkip(t *testing.T) {
285+ ctrl := gomock.NewController(t)
286+ defer ctrl.Finish()
287+
288+ mockFS := mocks.NewMockFileSystem(ctrl)
289+ mockSysExec := mocks.NewMockSystemExecutor(ctrl)
290+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
291+
292+ tmpDir := t.TempDir()
293+ logPath := filepath.Join(tmpDir, "test.log")
294+ log, err := logger.New(logPath, false)
295+ if err != nil {
296+ t.Fatalf("Failed to create logger: %v", err)
297+ }
298+ defer log.Close()
299+
300+ cfg := config.NewConfig("test")
301+ phase := NewConfigPhase(cfg, log, mockFS, mockSysExec, mockChrExec)
302+
303+ if phase.CanSkip() {
304+ t.Error("CanSkip() = true, want false")
305+ }
306+}
307+
308+// Helper function to check if a string contains a substring
309+func containsSubstring(s, substr string) bool {
310+ for i := 0; i < len(s)-len(substr)+1; i++ {
311+ if s[i:i+len(substr)] == substr {
312+ return true
313+ }
314+ }
315+ return false
316+}
317+
318+// mockFileInfo implements os.FileInfo for testing
319+type mockFileInfo struct{}
320+
321+func (m mockFileInfo) Name() string { return "mock" }
322+func (m mockFileInfo) Size() int64 { return 0 }
323+func (m mockFileInfo) Mode() os.FileMode { return 0 }
324+func (m mockFileInfo) ModTime() time.Time { return time.Now() }
325+func (m mockFileInfo) IsDir() bool { return false }
326+func (m mockFileInfo) Sys() interface{} { return nil }

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