1package disk
2
3import (
4 "testing"
5)
6
7// TestNewMountOptions tests MountOptions creation
8func TestNewMountOptions(t *testing.T) {
9 tests := []struct {
10 name string
11 options map[string]string
12 shouldErr bool
13 }{
14 {"valid empty", map[string]string{}, false},
15 {"valid with values", map[string]string{"compress": "zstd", "noatime": ""}, false},
16 {"valid subvol", map[string]string{"subvol": "@"}, false},
17 {"nil options", nil, true},
18 {"empty key", map[string]string{"": "value"}, true},
19 {"key with space", map[string]string{"no time": ""}, true},
20 {"key with comma", map[string]string{"no,time": ""}, true},
21 {"key with equals", map[string]string{"no=time": ""}, true},
22 {"value with space", map[string]string{"compress": "zstd level"}, true},
23 {"value with comma", map[string]string{"compress": "zstd,level"}, true},
24 }
25
26 for _, tt := range tests {
27 t.Run(tt.name, func(t *testing.T) {
28 opts, err := NewMountOptions(tt.options)
29
30 if (err != nil) != tt.shouldErr {
31 t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
32 }
33
34 if !tt.shouldErr && opts == nil {
35 t.Error("expected non-nil options, got nil")
36 }
37
38 // Verify immutability - original map should not affect internal state
39 if !tt.shouldErr && tt.options != nil {
40 tt.options["test"] = "value"
41 if opts.Has("test") {
42 t.Error("modifying original map should not affect MountOptions")
43 }
44 }
45 })
46 }
47}
48
49// TestNewBtrfsMountOptions tests standard Btrfs mount options creation
50func TestNewBtrfsMountOptions(t *testing.T) {
51 tests := []struct {
52 name string
53 subvolume string
54 }{
55 {"without subvolume", ""},
56 {"with @ subvolume", "@"},
57 {"with @home subvolume", "@home"},
58 }
59
60 for _, tt := range tests {
61 t.Run(tt.name, func(t *testing.T) {
62 opts, err := NewBtrfsMountOptions(tt.subvolume)
63 if err != nil {
64 t.Fatalf("failed to create Btrfs mount options: %v", err)
65 }
66
67 if !opts.Has("noatime") {
68 t.Error("Btrfs mount options should have noatime")
69 }
70
71 if !opts.Has("compress") {
72 t.Error("Btrfs mount options should have compress")
73 }
74
75 compress, _ := opts.Get("compress")
76 if compress != "zstd" {
77 t.Errorf("compress should be zstd, got %q", compress)
78 }
79
80 if tt.subvolume != "" {
81 if !opts.Has("subvol") {
82 t.Error("should have subvol option when subvolume is specified")
83 }
84 subvol, _ := opts.Get("subvol")
85 if subvol != tt.subvolume {
86 t.Errorf("subvol should be %q, got %q", tt.subvolume, subvol)
87 }
88 } else {
89 if opts.Has("subvol") {
90 t.Error("should not have subvol option when subvolume is empty")
91 }
92 }
93 })
94 }
95}
96
97// TestMountOptionsHas tests Has method
98func TestMountOptionsHas(t *testing.T) {
99 opts, _ := NewMountOptions(map[string]string{
100 "noatime": "",
101 "compress": "zstd",
102 })
103
104 if !opts.Has("noatime") {
105 t.Error("should have noatime option")
106 }
107
108 if !opts.Has("compress") {
109 t.Error("should have compress option")
110 }
111
112 if opts.Has("nonexistent") {
113 t.Error("should not have nonexistent option")
114 }
115}
116
117// TestMountOptionsGet tests Get method
118func TestMountOptionsGet(t *testing.T) {
119 opts, _ := NewMountOptions(map[string]string{
120 "noatime": "",
121 "compress": "zstd",
122 })
123
124 // Get existing option with value
125 value, exists := opts.Get("compress")
126 if !exists {
127 t.Error("compress option should exist")
128 }
129 if value != "zstd" {
130 t.Errorf("compress value should be zstd, got %q", value)
131 }
132
133 // Get existing option without value
134 value, exists = opts.Get("noatime")
135 if !exists {
136 t.Error("noatime option should exist")
137 }
138 if value != "" {
139 t.Errorf("noatime value should be empty, got %q", value)
140 }
141
142 // Get nonexistent option
143 _, exists = opts.Get("nonexistent")
144 if exists {
145 t.Error("nonexistent option should not exist")
146 }
147}
148
149// TestMountOptionsToString tests ToString method
150func TestMountOptionsToString(t *testing.T) {
151 tests := []struct {
152 name string
153 options map[string]string
154 expected []string // all possible orderings
155 }{
156 {
157 "empty",
158 map[string]string{},
159 []string{""},
160 },
161 {
162 "single flag",
163 map[string]string{"noatime": ""},
164 []string{"noatime"},
165 },
166 {
167 "single value",
168 map[string]string{"compress": "zstd"},
169 []string{"compress=zstd"},
170 },
171 {
172 "multiple options",
173 map[string]string{"noatime": "", "compress": "zstd"},
174 []string{
175 "noatime,compress=zstd",
176 "compress=zstd,noatime",
177 },
178 },
179 }
180
181 for _, tt := range tests {
182 t.Run(tt.name, func(t *testing.T) {
183 opts, _ := NewMountOptions(tt.options)
184 result := opts.ToString()
185
186 // Check if result matches any expected ordering
187 found := false
188 for _, expected := range tt.expected {
189 if result == expected {
190 found = true
191 break
192 }
193 }
194
195 if !found {
196 t.Errorf("ToString() = %q, want one of %v", result, tt.expected)
197 }
198 })
199 }
200}
201
202// TestMountOptionsEquals tests Equals method
203func TestMountOptionsEquals(t *testing.T) {
204 opts1, _ := NewMountOptions(map[string]string{"noatime": "", "compress": "zstd"})
205 opts2, _ := NewMountOptions(map[string]string{"noatime": "", "compress": "zstd"})
206 opts3, _ := NewMountOptions(map[string]string{"noatime": ""})
207 opts4, _ := NewMountOptions(map[string]string{"noatime": "", "compress": "lzo"})
208
209 if !opts1.Equals(opts2) {
210 t.Error("identical options should be equal")
211 }
212
213 if opts1.Equals(opts3) {
214 t.Error("options with different keys should not be equal")
215 }
216
217 if opts1.Equals(opts4) {
218 t.Error("options with different values should not be equal")
219 }
220
221 if opts1.Equals(nil) {
222 t.Error("options should not equal nil")
223 }
224}
225
226// TestMountOptionsOptions tests Options method (immutability)
227func TestMountOptionsOptions(t *testing.T) {
228 opts, _ := NewMountOptions(map[string]string{"noatime": "", "compress": "zstd"})
229 options := opts.Options()
230
231 // Modify returned map
232 options["test"] = "value"
233
234 // Verify internal state is not affected
235 if opts.Has("test") {
236 t.Error("modifying returned options should not affect MountOptions")
237 }
238}
239
240// TestParseMountOptions tests ParseMountOptions function
241func TestParseMountOptions(t *testing.T) {
242 tests := []struct {
243 name string
244 optionsStr string
245 shouldErr bool
246 expected map[string]string
247 }{
248 {"empty", "", false, map[string]string{}},
249 {"single flag", "noatime", false, map[string]string{"noatime": ""}},
250 {"single value", "compress=zstd", false, map[string]string{"compress": "zstd"}},
251 {"multiple options", "noatime,compress=zstd", false, map[string]string{"noatime": "", "compress": "zstd"}},
252 {"with subvol", "noatime,compress=zstd,subvol=@", false, map[string]string{"noatime": "", "compress": "zstd", "subvol": "@"}},
253 {"with spaces", " noatime , compress=zstd ", false, map[string]string{"noatime": "", "compress": "zstd"}},
254 {"invalid format", "compress=", false, map[string]string{"compress": ""}},
255 {"extra commas", "noatime,,compress=zstd", false, map[string]string{"noatime": "", "compress": "zstd"}},
256 }
257
258 for _, tt := range tests {
259 t.Run(tt.name, func(t *testing.T) {
260 opts, err := ParseMountOptions(tt.optionsStr)
261
262 if (err != nil) != tt.shouldErr {
263 t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
264 }
265
266 if !tt.shouldErr && opts != nil {
267 for key, expectedValue := range tt.expected {
268 value, exists := opts.Get(key)
269 if !exists {
270 t.Errorf("expected key %q to exist", key)
271 }
272 if value != expectedValue {
273 t.Errorf("key %q: got value %q, want %q", key, value, expectedValue)
274 }
275 }
276
277 // Verify no extra keys
278 gotOptions := opts.Options()
279 if len(gotOptions) != len(tt.expected) {
280 t.Errorf("got %d options, want %d", len(gotOptions), len(tt.expected))
281 }
282 }
283 })
284 }
285}
286
287// TestNewMountPoint tests MountPoint creation
288func TestNewMountPoint(t *testing.T) {
289 validOpts, _ := NewMountOptions(map[string]string{"noatime": ""})
290
291 tests := []struct {
292 name string
293 path string
294 device string
295 options *MountOptions
296 shouldErr bool
297 }{
298 {"valid root", "/", "/dev/sda1", validOpts, false},
299 {"valid home", "/home", "/dev/sda2", validOpts, false},
300 {"valid boot", "/boot", "/dev/sda1", validOpts, false},
301 {"empty path", "", "/dev/sda1", validOpts, true},
302 {"path no slash", "home", "/dev/sda1", validOpts, true},
303 {"empty device", "/home", "", validOpts, true},
304 {"nil options", "/home", "/dev/sda1", nil, true},
305 }
306
307 for _, tt := range tests {
308 t.Run(tt.name, func(t *testing.T) {
309 mp, err := NewMountPoint(tt.path, tt.device, tt.options)
310
311 if (err != nil) != tt.shouldErr {
312 t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
313 }
314
315 if !tt.shouldErr && mp == nil {
316 t.Error("expected non-nil mount point, got nil")
317 }
318
319 if !tt.shouldErr && mp != nil {
320 if mp.Path() != tt.path {
321 t.Errorf("got path %q, want %q", mp.Path(), tt.path)
322 }
323 if mp.Device() != tt.device {
324 t.Errorf("got device %q, want %q", mp.Device(), tt.device)
325 }
326 if !mp.Options().Equals(tt.options) {
327 t.Error("options should match")
328 }
329 }
330 })
331 }
332}
333
334// TestMountPointIsRoot tests IsRoot method
335func TestMountPointIsRoot(t *testing.T) {
336 opts, _ := NewMountOptions(map[string]string{"noatime": ""})
337
338 root, _ := NewMountPoint("/", "/dev/sda1", opts)
339 if !root.IsRoot() {
340 t.Error("/ should be root mount point")
341 }
342
343 home, _ := NewMountPoint("/home", "/dev/sda2", opts)
344 if home.IsRoot() {
345 t.Error("/home should not be root mount point")
346 }
347}
348
349// TestMountPointEquals tests Equals method
350func TestMountPointEquals(t *testing.T) {
351 opts1, _ := NewMountOptions(map[string]string{"noatime": ""})
352 opts2, _ := NewMountOptions(map[string]string{"noatime": ""})
353 opts3, _ := NewMountOptions(map[string]string{"compress": "zstd"})
354
355 mp1, _ := NewMountPoint("/", "/dev/sda1", opts1)
356 mp2, _ := NewMountPoint("/", "/dev/sda1", opts2)
357 mp3, _ := NewMountPoint("/home", "/dev/sda2", opts1)
358 mp4, _ := NewMountPoint("/", "/dev/sda1", opts3)
359
360 if !mp1.Equals(mp2) {
361 t.Error("identical mount points should be equal")
362 }
363
364 if mp1.Equals(mp3) {
365 t.Error("mount points with different paths should not be equal")
366 }
367
368 if mp1.Equals(mp4) {
369 t.Error("mount points with different options should not be equal")
370 }
371
372 if mp1.Equals(nil) {
373 t.Error("mount point should not equal nil")
374 }
375}
376
377// TestValidateMountPoint tests ValidateMountPoint function
378func TestValidateMountPoint(t *testing.T) {
379 tests := []struct {
380 name string
381 path string
382 shouldErr bool
383 }{
384 {"valid root", "/", false},
385 {"valid home", "/home", false},
386 {"valid nested", "/var/log", false},
387 {"empty", "", true},
388 {"no slash", "home", true},
389 {"double slash", "/home//data", true},
390 {"dot dot", "/home/../data", true},
391 {"too long", "/" + string(make([]byte, 256)), true},
392 }
393
394 for _, tt := range tests {
395 t.Run(tt.name, func(t *testing.T) {
396 err := ValidateMountPoint(tt.path)
397 if (err != nil) != tt.shouldErr {
398 t.Errorf("got error %v, expected error=%v", err, tt.shouldErr)
399 }
400 })
401 }
402}
403
404// TestMountOptionsString tests String method
405func TestMountOptionsString(t *testing.T) {
406 opts, _ := NewMountOptions(map[string]string{"noatime": ""})
407 str := opts.String()
408 if !contains(str, "MountOptions") {
409 t.Errorf("String() should contain MountOptions: %s", str)
410 }
411}
412
413// TestMountPointString tests String method
414func TestMountPointString(t *testing.T) {
415 opts, _ := NewMountOptions(map[string]string{"noatime": ""})
416 mp, _ := NewMountPoint("/", "/dev/sda1", opts)
417 str := mp.String()
418 if !contains(str, "MountPoint") || !contains(str, "/") || !contains(str, "/dev/sda1") {
419 t.Errorf("String() should contain path and device: %s", str)
420 }
421}