main 6af0a61

system_test.go RawHistoryFolder

1package system
2
3import (
4	"context"
5	"errors"
6	"strings"
7	"testing"
8
9	"github.com/bnema/archup/internal/domain/ports/mocks"
10	"go.uber.org/mock/gomock"
11)
12
13// SystemConfig Tests
14
15func TestNewSystemConfig_Valid(t *testing.T) {
16	config, err := NewSystemConfig("myhost", "UTC", "en_US.UTF-8", "us")
17
18	if err != nil {
19		t.Fatalf("expected no error, got %v", err)
20	}
21
22	if config.Hostname() != "myhost" {
23		t.Errorf("expected hostname 'myhost', got '%s'", config.Hostname())
24	}
25
26	if config.Timezone() != "UTC" {
27		t.Errorf("expected timezone 'UTC', got '%s'", config.Timezone())
28	}
29
30	if config.Locale() != "en_US.UTF-8" {
31		t.Errorf("expected locale 'en_US.UTF-8', got '%s'", config.Locale())
32	}
33
34	if config.Keymap() != "us" {
35		t.Errorf("expected keymap 'us', got '%s'", config.Keymap())
36	}
37}
38
39func TestNewSystemConfig_InvalidHostname(t *testing.T) {
40	tests := []struct {
41		name      string
42		hostname  string
43		shouldErr bool
44	}{
45		{"empty hostname", "", true},
46		{"starts with hyphen", "-host", true},
47		{"ends with hyphen", "host-", true},
48		{"too long", "a" + string(make([]byte, 100)), true},
49		{"valid hostname", "myhost", false},
50		{"with hyphen", "my-host", false},
51		{"with digits", "host123", false},
52	}
53
54	for _, tt := range tests {
55		t.Run(tt.name, func(t *testing.T) {
56			_, err := NewSystemConfig(tt.hostname, "UTC", "en_US.UTF-8", "us")
57			if (err != nil) != tt.shouldErr {
58				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
59			}
60		})
61	}
62}
63
64func TestNewSystemConfig_InvalidLocale(t *testing.T) {
65	tests := []struct {
66		name      string
67		locale    string
68		shouldErr bool
69	}{
70		{"valid locale", "en_US.UTF-8", false},
71		{"empty locale allowed", "", false},
72		{"C locale", "C", false},
73		{"C.UTF-8", "C.UTF-8", false},
74		{"too long", "a" + string(make([]byte, 100)), true},
75	}
76
77	for _, tt := range tests {
78		t.Run(tt.name, func(t *testing.T) {
79			_, err := NewSystemConfig("host", "UTC", tt.locale, "us")
80			if (err != nil) != tt.shouldErr {
81				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
82			}
83		})
84	}
85}
86
87func TestNewSystemConfig_InvalidKeymap(t *testing.T) {
88	tests := []struct {
89		name      string
90		keymap    string
91		shouldErr bool
92	}{
93		{"valid simple keymap", "us", false},
94		{"with variant", "de-nodeadkeys", false},
95		{"empty keymap", "", true},
96		{"with uppercase", "US", true},
97		{"with special chars", "us!", true},
98		{"too long", "a" + string(make([]byte, 100)), true},
99	}
100
101	for _, tt := range tests {
102		t.Run(tt.name, func(t *testing.T) {
103			_, err := NewSystemConfig("host", "UTC", "en_US.UTF-8", tt.keymap)
104			if (err != nil) != tt.shouldErr {
105				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
106			}
107		})
108	}
109}
110
111func TestSystemConfig_Equals(t *testing.T) {
112	config1, _ := NewSystemConfig("host1", "UTC", "en_US.UTF-8", "us")
113	config2, _ := NewSystemConfig("host1", "UTC", "en_US.UTF-8", "us")
114	config3, _ := NewSystemConfig("host2", "UTC", "en_US.UTF-8", "us")
115
116	if !config1.Equals(config2) {
117		t.Error("expected equal configs")
118	}
119
120	if config1.Equals(config3) {
121		t.Error("expected different configs")
122	}
123
124	if config1.Equals(nil) {
125		t.Error("expected nil to not be equal")
126	}
127}
128
129func TestValidateHostname(t *testing.T) {
130	tests := []struct {
131		name      string
132		hostname  string
133		shouldErr bool
134	}{
135		{"valid", "myhost", false},
136		{"with digits", "host123", false},
137		{"with hyphen", "my-host", false},
138		{"empty", "", true},
139		{"starts with hyphen", "-host", true},
140		{"ends with hyphen", "host-", true},
141		{"contains underscore", "my_host", true},
142		{"contains space", "my host", true},
143	}
144
145	for _, tt := range tests {
146		t.Run(tt.name, func(t *testing.T) {
147			err := ValidateHostname(tt.hostname)
148			if (err != nil) != tt.shouldErr {
149				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
150			}
151		})
152	}
153}
154
155func TestValidateTimezone(t *testing.T) {
156	tests := []struct {
157		name      string
158		timezone  string
159		shouldErr bool
160	}{
161		{"UTC", "UTC", false},
162		{"valid timezone", "America/New_York", false},
163		{"empty allowed", "", false},
164		{"contains ..", "UTC..", true},
165		{"starts with /", "/UTC", true},
166		{"too long", "a" + string(make([]byte, 100)), true},
167	}
168
169	for _, tt := range tests {
170		t.Run(tt.name, func(t *testing.T) {
171			err := ValidateTimezone(tt.timezone)
172			if (err != nil) != tt.shouldErr {
173				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
174			}
175		})
176	}
177}
178
179func TestValidateLocale(t *testing.T) {
180	tests := []struct {
181		name      string
182		locale    string
183		shouldErr bool
184	}{
185		{"valid UTF-8", "en_US.UTF-8", false},
186		{"C locale", "C", false},
187		{"empty allowed", "", false},
188		{"ISO variant", "en_US.ISO8859-1", false},
189		{"too long", "a" + string(make([]byte, 100)), true},
190	}
191
192	for _, tt := range tests {
193		t.Run(tt.name, func(t *testing.T) {
194			err := ValidateLocale(tt.locale)
195			if (err != nil) != tt.shouldErr {
196				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
197			}
198		})
199	}
200}
201
202func TestValidateKeymap(t *testing.T) {
203	tests := []struct {
204		name      string
205		keymap    string
206		shouldErr bool
207	}{
208		{"us", "us", false},
209		{"de", "de", false},
210		{"with variant", "de-nodeadkeys", false},
211		{"empty", "", true},
212		{"uppercase", "US", true},
213		{"with special chars", "us!", true},
214	}
215
216	for _, tt := range tests {
217		t.Run(tt.name, func(t *testing.T) {
218			err := ValidateKeymap(tt.keymap)
219			if (err != nil) != tt.shouldErr {
220				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
221			}
222		})
223	}
224}
225
226// CPUInfo Tests
227
228func TestNewCPUInfo_Valid(t *testing.T) {
229	cpuInfo, err := NewCPUInfo(CPUVendorIntel, "Intel(R) Core(TM) i7-9700K", MicrocodeIntel)
230
231	if err != nil {
232		t.Fatalf("expected no error, got %v", err)
233	}
234
235	if cpuInfo.Vendor() != CPUVendorIntel {
236		t.Errorf("expected Intel vendor")
237	}
238
239	if !cpuInfo.RequiresMicrocode() {
240		t.Error("expected to require microcode")
241	}
242}
243
244func TestNewCPUInfo_InvalidModel(t *testing.T) {
245	tests := []struct {
246		name      string
247		model     string
248		shouldErr bool
249	}{
250		{"valid", "Intel Core i7", false},
251		{"empty", "", true},
252		{"too long", "a" + string(make([]byte, 200)), true},
253	}
254
255	for _, tt := range tests {
256		t.Run(tt.name, func(t *testing.T) {
257			_, err := NewCPUInfo(CPUVendorIntel, tt.model, MicrocodeIntel)
258			if (err != nil) != tt.shouldErr {
259				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
260			}
261		})
262	}
263}
264
265func TestNewCPUInfo_MicrocodeVendorMismatch(t *testing.T) {
266	tests := []struct {
267		name      string
268		vendor    CPUVendor
269		microcode Microcode
270		shouldErr bool
271	}{
272		{"Intel with Intel ucode", CPUVendorIntel, MicrocodeIntel, false},
273		{"Intel with AMD ucode", CPUVendorIntel, MicrocodeAMD, true},
274		{"AMD with AMD ucode", CPUVendorAMD, MicrocodeAMD, false},
275		{"AMD with Intel ucode", CPUVendorAMD, MicrocodeIntel, true},
276		{"Intel with no ucode", CPUVendorIntel, MicrocodeNone, false},
277	}
278
279	for _, tt := range tests {
280		t.Run(tt.name, func(t *testing.T) {
281			_, err := NewCPUInfo(tt.vendor, "Model", tt.microcode)
282			if (err != nil) != tt.shouldErr {
283				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
284			}
285		})
286	}
287}
288
289func TestCPUInfo_Equals(t *testing.T) {
290	cpu1, _ := NewCPUInfo(CPUVendorIntel, "i7", MicrocodeIntel)
291	cpu2, _ := NewCPUInfo(CPUVendorIntel, "i7", MicrocodeIntel)
292	cpu3, _ := NewCPUInfo(CPUVendorAMD, "Ryzen", MicrocodeAMD)
293
294	if !cpu1.Equals(cpu2) {
295		t.Error("expected equal CPUInfo")
296	}
297
298	if cpu1.Equals(cpu3) {
299		t.Error("expected different CPUInfo")
300	}
301
302	if cpu1.Equals(nil) {
303		t.Error("expected nil to not be equal")
304	}
305}
306
307func TestCPUVendor_String(t *testing.T) {
308	tests := []struct {
309		vendor CPUVendor
310		want   string
311	}{
312		{CPUVendorIntel, "Intel"},
313		{CPUVendorAMD, "AMD"},
314		{CPUVendorARM, "ARM"},
315		{CPUVendorUnknown, "Unknown"},
316	}
317
318	for _, tt := range tests {
319		if got := tt.vendor.String(); got != tt.want {
320			t.Errorf("expected %s, got %s", tt.want, got)
321		}
322	}
323}
324
325func TestMicrocode_String(t *testing.T) {
326	tests := []struct {
327		microcode Microcode
328		want      string
329	}{
330		{MicrocodeIntel, "intel-ucode"},
331		{MicrocodeAMD, "amd-ucode"},
332		{MicrocodeNone, ""},
333	}
334
335	for _, tt := range tests {
336		if got := tt.microcode.String(); got != tt.want {
337			t.Errorf("expected %s, got %s", tt.want, got)
338		}
339	}
340}
341
342// ValidationRules Tests
343
344func TestSanitizeHostname(t *testing.T) {
345	rules := NewSystemValidationRules()
346
347	tests := []struct {
348		input    string
349		expected string
350	}{
351		{"MyHost", "myhost"},
352		{"my-host", "my-host"},
353		{"my_host", "my-host"},
354		{"-myhost", "myhost"},
355		{"myhost-", "myhost"},
356		{"my--host", "my-host"},
357		{"my__host", "my-host"},
358		{"MyHost123", "myhost123"},
359		{strings.Repeat("a", 100), strings.Repeat("a", 63)},
360	}
361
362	for _, tt := range tests {
363		t.Run(tt.input[:min(10, len(tt.input))], func(t *testing.T) {
364			got := rules.SanitizeHostname(tt.input)
365			if got != tt.expected {
366				t.Errorf("expected '%s', got '%s'", tt.expected, got)
367			}
368
369			// Verify result passes validation
370			if err := ValidateHostname(got); err != nil {
371				t.Errorf("sanitized hostname failed validation: %v", err)
372			}
373		})
374	}
375}
376
377func TestValidateCombination(t *testing.T) {
378	rules := NewSystemValidationRules()
379
380	config, _ := NewSystemConfig("host", "UTC", "en_US.UTF-8", "us")
381	cpu, _ := NewCPUInfo(CPUVendorIntel, "i7", MicrocodeIntel)
382
383	err := rules.ValidateCombination(config, cpu)
384	if err != nil {
385		t.Errorf("expected no error, got %v", err)
386	}
387
388	// Test nil checks
389	if err := rules.ValidateCombination(nil, cpu); err == nil {
390		t.Error("expected error for nil config")
391	}
392
393	if err := rules.ValidateCombination(config, nil); err == nil {
394		t.Error("expected error for nil cpu")
395	}
396}
397
398func TestSuggestedLocaleForTimezone(t *testing.T) {
399	rules := NewSystemValidationRules()
400
401	tests := []struct {
402		timezone string
403		expected string
404	}{
405		{"UTC", "en_US.UTF-8"},
406		{"America/New_York", "en_US.UTF-8"},
407		{"Europe/London", "en_GB.UTF-8"},
408		{"Europe/Paris", "fr_FR.UTF-8"},
409		{"Unknown/Zone", ""},
410	}
411
412	for _, tt := range tests {
413		t.Run(tt.timezone, func(t *testing.T) {
414			got := rules.SuggestedLocaleForTimezone(tt.timezone)
415			if got != tt.expected {
416				t.Errorf("expected '%s', got '%s'", tt.expected, got)
417			}
418		})
419	}
420}
421
422func TestSuggestedKeymapForLocale(t *testing.T) {
423	rules := NewSystemValidationRules()
424
425	tests := []struct {
426		locale   string
427		expected string
428	}{
429		{"en_US.UTF-8", "us"},
430		{"en_GB.UTF-8", "gb"},
431		{"de_DE.UTF-8", "de"},
432		{"fr_FR.UTF-8", "fr"},
433		{"unknown", ""},
434	}
435
436	for _, tt := range tests {
437		t.Run(tt.locale, func(t *testing.T) {
438			got := rules.SuggestedKeymapForLocale(tt.locale)
439			if got != tt.expected {
440				t.Errorf("expected '%s', got '%s'", tt.expected, got)
441			}
442		})
443	}
444}
445
446// New validation function tests
447
448func TestValidateArchLinux(t *testing.T) {
449	ctrl := gomock.NewController(t)
450	defer ctrl.Finish()
451
452	rules := NewSystemValidationRules()
453	ctx := context.Background()
454
455	tests := []struct {
456		name       string
457		fileExists bool
458		shouldErr  bool
459	}{
460		{"arch-release exists", true, false},
461		{"arch-release missing", false, true},
462	}
463
464	for _, tt := range tests {
465		t.Run(tt.name, func(t *testing.T) {
466			mockFS := mocks.NewMockFileSystem(ctrl)
467			mockFS.EXPECT().Exists("/etc/arch-release").Return(tt.fileExists, nil)
468
469			err := rules.ValidateArchLinux(ctx, mockFS)
470			if (err != nil) != tt.shouldErr {
471				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
472			}
473		})
474	}
475}
476
477func TestValidateNotDerivative(t *testing.T) {
478	rules := NewSystemValidationRules()
479	ctx := context.Background()
480
481	tests := []struct {
482		name      string
483		setupMock func(*mocks.MockFileSystem)
484		shouldErr bool
485		errMsg    string
486	}{
487		{
488			name: "vanilla arch",
489			setupMock: func(mockFS *mocks.MockFileSystem) {
490				mockFS.EXPECT().Exists("/etc/cachyos-release").Return(false, nil)
491				mockFS.EXPECT().Exists("/etc/eos-release").Return(false, nil)
492				mockFS.EXPECT().Exists("/etc/garuda-release").Return(false, nil)
493				mockFS.EXPECT().Exists("/etc/manjaro-release").Return(false, nil)
494			},
495			shouldErr: false,
496		},
497		{
498			name: "cachyos detected",
499			setupMock: func(mockFS *mocks.MockFileSystem) {
500				// Map iteration order is undefined, so any may be checked first
501				mockFS.EXPECT().Exists("/etc/cachyos-release").Return(true, nil).AnyTimes()
502				mockFS.EXPECT().Exists("/etc/eos-release").Return(false, nil).AnyTimes()
503				mockFS.EXPECT().Exists("/etc/garuda-release").Return(false, nil).AnyTimes()
504				mockFS.EXPECT().Exists("/etc/manjaro-release").Return(false, nil).AnyTimes()
505			},
506			shouldErr: true,
507			errMsg:    "CachyOS",
508		},
509		{
510			name: "endeavouros detected",
511			setupMock: func(mockFS *mocks.MockFileSystem) {
512				mockFS.EXPECT().Exists("/etc/cachyos-release").Return(false, nil).AnyTimes()
513				mockFS.EXPECT().Exists("/etc/eos-release").Return(true, nil).AnyTimes()
514				mockFS.EXPECT().Exists("/etc/garuda-release").Return(false, nil).AnyTimes()
515				mockFS.EXPECT().Exists("/etc/manjaro-release").Return(false, nil).AnyTimes()
516			},
517			shouldErr: true,
518			errMsg:    "EndeavourOS",
519		},
520		{
521			name: "garuda detected",
522			setupMock: func(mockFS *mocks.MockFileSystem) {
523				mockFS.EXPECT().Exists("/etc/cachyos-release").Return(false, nil).AnyTimes()
524				mockFS.EXPECT().Exists("/etc/eos-release").Return(false, nil).AnyTimes()
525				mockFS.EXPECT().Exists("/etc/garuda-release").Return(true, nil).AnyTimes()
526				mockFS.EXPECT().Exists("/etc/manjaro-release").Return(false, nil).AnyTimes()
527			},
528			shouldErr: true,
529			errMsg:    "Garuda",
530		},
531		{
532			name: "manjaro detected",
533			setupMock: func(mockFS *mocks.MockFileSystem) {
534				mockFS.EXPECT().Exists("/etc/cachyos-release").Return(false, nil).AnyTimes()
535				mockFS.EXPECT().Exists("/etc/eos-release").Return(false, nil).AnyTimes()
536				mockFS.EXPECT().Exists("/etc/garuda-release").Return(false, nil).AnyTimes()
537				mockFS.EXPECT().Exists("/etc/manjaro-release").Return(true, nil).AnyTimes()
538			},
539			shouldErr: true,
540			errMsg:    "Manjaro",
541		},
542	}
543
544	for _, tt := range tests {
545		t.Run(tt.name, func(t *testing.T) {
546			ctrl := gomock.NewController(t)
547			defer ctrl.Finish()
548
549			mockFS := mocks.NewMockFileSystem(ctrl)
550			tt.setupMock(mockFS)
551
552			err := rules.ValidateNotDerivative(ctx, mockFS)
553			if (err != nil) != tt.shouldErr {
554				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
555			}
556			if tt.shouldErr && err != nil && !strings.Contains(err.Error(), tt.errMsg) {
557				t.Errorf("expected error to contain '%s', got '%s'", tt.errMsg, err.Error())
558			}
559		})
560	}
561}
562
563func TestValidateArchitecture(t *testing.T) {
564	rules := NewSystemValidationRules()
565
566	tests := []struct {
567		name      string
568		arch      string
569		shouldErr bool
570	}{
571		{"x86_64", "x86_64", false},
572		{"empty", "", true},
573		{"aarch64", "aarch64", true},
574		{"i686", "i686", true},
575	}
576
577	for _, tt := range tests {
578		t.Run(tt.name, func(t *testing.T) {
579			err := rules.ValidateArchitecture(tt.arch)
580			if (err != nil) != tt.shouldErr {
581				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
582			}
583		})
584	}
585}
586
587func TestValidateUEFIBoot(t *testing.T) {
588	ctrl := gomock.NewController(t)
589	defer ctrl.Finish()
590
591	rules := NewSystemValidationRules()
592	ctx := context.Background()
593
594	tests := []struct {
595		name          string
596		efivarsExists bool
597		shouldErr     bool
598	}{
599		{"efivars exists", true, false},
600		{"efivars missing", false, true},
601	}
602
603	for _, tt := range tests {
604		t.Run(tt.name, func(t *testing.T) {
605			mockFS := mocks.NewMockFileSystem(ctrl)
606			mockFS.EXPECT().Exists("/sys/firmware/efi/efivars").Return(tt.efivarsExists, nil)
607
608			err := rules.ValidateUEFIBoot(ctx, mockFS)
609			if (err != nil) != tt.shouldErr {
610				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
611			}
612		})
613	}
614}
615
616func TestDetectSecureBoot(t *testing.T) {
617	ctrl := gomock.NewController(t)
618	defer ctrl.Finish()
619
620	rules := NewSystemValidationRules()
621	ctx := context.Background()
622
623	tests := []struct {
624		name     string
625		output   []byte
626		cmdErr   error
627		expected bool
628	}{
629		{
630			name:     "secure boot enabled",
631			output:   []byte("System:\n  Secure Boot: enabled\n"),
632			expected: true,
633		},
634		{
635			name:     "secure boot disabled",
636			output:   []byte("System:\n  Secure Boot: disabled\n"),
637			expected: false,
638		},
639		{
640			name:     "bootctl fails",
641			cmdErr:   errors.New("bootctl not found"),
642			expected: false,
643		},
644	}
645
646	for _, tt := range tests {
647		t.Run(tt.name, func(t *testing.T) {
648			mockExec := mocks.NewMockCommandExecutor(ctrl)
649			if tt.cmdErr != nil {
650				mockExec.EXPECT().Execute(ctx, "bootctl", "status").Return(nil, tt.cmdErr)
651			} else {
652				mockExec.EXPECT().Execute(ctx, "bootctl", "status").Return(tt.output, nil)
653			}
654
655			enabled, err := rules.DetectSecureBoot(ctx, mockExec)
656			if err != nil {
657				t.Errorf("unexpected error: %v", err)
658			}
659			if enabled != tt.expected {
660				t.Errorf("expected %v, got %v", tt.expected, enabled)
661			}
662		})
663	}
664}
665
666func TestValidateSecureBootDisabled(t *testing.T) {
667	ctrl := gomock.NewController(t)
668	defer ctrl.Finish()
669
670	rules := NewSystemValidationRules()
671	ctx := context.Background()
672
673	tests := []struct {
674		name      string
675		output    []byte
676		shouldErr bool
677	}{
678		{
679			name:      "secure boot disabled",
680			output:    []byte("System:\n  Secure Boot: disabled\n"),
681			shouldErr: false,
682		},
683		{
684			name:      "secure boot enabled",
685			output:    []byte("System:\n  Secure Boot: enabled\n"),
686			shouldErr: true,
687		},
688	}
689
690	for _, tt := range tests {
691		t.Run(tt.name, func(t *testing.T) {
692			mockExec := mocks.NewMockCommandExecutor(ctrl)
693			mockExec.EXPECT().Execute(ctx, "bootctl", "status").Return(tt.output, nil)
694
695			err := rules.ValidateSecureBootDisabled(ctx, mockExec)
696			if (err != nil) != tt.shouldErr {
697				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
698			}
699		})
700	}
701}
702
703func TestDistributionType_String(t *testing.T) {
704	tests := []struct {
705		distro DistributionType
706		want   string
707	}{
708		{DistributionArch, "Arch Linux"},
709		{DistributionCachyOS, "CachyOS"},
710		{DistributionEndeavourOS, "EndeavourOS"},
711		{DistributionGaruda, "Garuda Linux"},
712		{DistributionManjaro, "Manjaro Linux"},
713		{DistributionUnknown, "Unknown"},
714	}
715
716	for _, tt := range tests {
717		if got := tt.distro.String(); got != tt.want {
718			t.Errorf("expected %s, got %s", tt.want, got)
719		}
720	}
721}
722
723func TestDetectDistribution(t *testing.T) {
724	ctrl := gomock.NewController(t)
725	defer ctrl.Finish()
726
727	ctx := context.Background()
728
729	tests := []struct {
730		name      string
731		setupMock func(*mocks.MockFileSystem)
732		expected  DistributionType
733		shouldErr bool
734	}{
735		{
736			name: "vanilla arch",
737			setupMock: func(mockFS *mocks.MockFileSystem) {
738				mockFS.EXPECT().Exists("/etc/arch-release").Return(true, nil)
739				mockFS.EXPECT().Exists("/etc/cachyos-release").Return(false, nil)
740				mockFS.EXPECT().Exists("/etc/eos-release").Return(false, nil)
741				mockFS.EXPECT().Exists("/etc/garuda-release").Return(false, nil)
742				mockFS.EXPECT().Exists("/etc/manjaro-release").Return(false, nil)
743			},
744			expected: DistributionArch,
745		},
746		{
747			name: "cachyos",
748			setupMock: func(mockFS *mocks.MockFileSystem) {
749				mockFS.EXPECT().Exists("/etc/arch-release").Return(true, nil)
750				mockFS.EXPECT().Exists("/etc/cachyos-release").Return(true, nil)
751			},
752			expected: DistributionCachyOS,
753		},
754		{
755			name: "endeavouros",
756			setupMock: func(mockFS *mocks.MockFileSystem) {
757				mockFS.EXPECT().Exists("/etc/arch-release").Return(true, nil)
758				mockFS.EXPECT().Exists("/etc/cachyos-release").Return(false, nil)
759				mockFS.EXPECT().Exists("/etc/eos-release").Return(true, nil)
760			},
761			expected: DistributionEndeavourOS,
762		},
763		{
764			name: "garuda",
765			setupMock: func(mockFS *mocks.MockFileSystem) {
766				mockFS.EXPECT().Exists("/etc/arch-release").Return(true, nil)
767				mockFS.EXPECT().Exists("/etc/cachyos-release").Return(false, nil)
768				mockFS.EXPECT().Exists("/etc/eos-release").Return(false, nil)
769				mockFS.EXPECT().Exists("/etc/garuda-release").Return(true, nil)
770			},
771			expected: DistributionGaruda,
772		},
773		{
774			name: "manjaro",
775			setupMock: func(mockFS *mocks.MockFileSystem) {
776				mockFS.EXPECT().Exists("/etc/arch-release").Return(true, nil)
777				mockFS.EXPECT().Exists("/etc/cachyos-release").Return(false, nil)
778				mockFS.EXPECT().Exists("/etc/eos-release").Return(false, nil)
779				mockFS.EXPECT().Exists("/etc/garuda-release").Return(false, nil)
780				mockFS.EXPECT().Exists("/etc/manjaro-release").Return(true, nil)
781			},
782			expected: DistributionManjaro,
783		},
784		{
785			name: "not arch",
786			setupMock: func(mockFS *mocks.MockFileSystem) {
787				mockFS.EXPECT().Exists("/etc/arch-release").Return(false, nil)
788			},
789			expected:  DistributionUnknown,
790			shouldErr: true,
791		},
792	}
793
794	for _, tt := range tests {
795		t.Run(tt.name, func(t *testing.T) {
796			mockFS := mocks.NewMockFileSystem(ctrl)
797			tt.setupMock(mockFS)
798
799			distro, err := DetectDistribution(ctx, mockFS)
800			if (err != nil) != tt.shouldErr {
801				t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
802			}
803			if distro != tt.expected {
804				t.Errorf("expected %v, got %v", tt.expected, distro)
805			}
806		})
807	}
808}
809
810// Helper function
811func min(a, b int) int {
812	if a < b {
813		return a
814	}
815	return b
816}

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