master
go 599 lines 18.5 KB
Raw
1 package config
2
3 import (
4 "strings"
5 "testing"
6
7 "github.com/ipfs/boxo/ipld/unixfs/io"
8 mh "github.com/multiformats/go-multihash"
9 )
10
11 func TestValidateImportConfig_HAMTFanout(t *testing.T) {
12 tests := []struct {
13 name string
14 fanout int64
15 wantErr bool
16 errMsg string
17 }{
18 // Valid values - powers of 2, multiples of 8, and <= 1024
19 {name: "valid 8", fanout: 8, wantErr: false},
20 {name: "valid 16", fanout: 16, wantErr: false},
21 {name: "valid 32", fanout: 32, wantErr: false},
22 {name: "valid 64", fanout: 64, wantErr: false},
23 {name: "valid 128", fanout: 128, wantErr: false},
24 {name: "valid 256", fanout: 256, wantErr: false},
25 {name: "valid 512", fanout: 512, wantErr: false},
26 {name: "valid 1024", fanout: 1024, wantErr: false},
27
28 // Invalid values - not powers of 2
29 {name: "invalid 7", fanout: 7, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
30 {name: "invalid 15", fanout: 15, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
31 {name: "invalid 100", fanout: 100, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
32 {name: "invalid 257", fanout: 257, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
33 {name: "invalid 1000", fanout: 1000, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
34
35 // Invalid values - powers of 2 but less than 8
36 {name: "invalid 1", fanout: 1, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
37 {name: "invalid 2", fanout: 2, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
38 {name: "invalid 4", fanout: 4, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
39
40 // Invalid values - exceeds 1024
41 {name: "invalid 2048", fanout: 2048, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
42 {name: "invalid 4096", fanout: 4096, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
43
44 // Invalid values - negative or zero
45 {name: "invalid 0", fanout: 0, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
46 {name: "invalid -8", fanout: -8, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
47 {name: "invalid -256", fanout: -256, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
48 }
49
50 for _, tt := range tests {
51 t.Run(tt.name, func(t *testing.T) {
52 cfg := &Import{
53 UnixFSHAMTDirectoryMaxFanout: *NewOptionalInteger(tt.fanout),
54 }
55
56 err := ValidateImportConfig(cfg)
57
58 if tt.wantErr {
59 if err == nil {
60 t.Errorf("ValidateImportConfig() expected error for fanout=%d, got nil", tt.fanout)
61 } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
62 t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
63 }
64 } else {
65 if err != nil {
66 t.Errorf("ValidateImportConfig() unexpected error for fanout=%d: %v", tt.fanout, err)
67 }
68 }
69 })
70 }
71 }
72
73 func TestValidateImportConfig_CidVersion(t *testing.T) {
74 tests := []struct {
75 name string
76 cidVer int64
77 wantErr bool
78 errMsg string
79 }{
80 {name: "valid 0", cidVer: 0, wantErr: false},
81 {name: "valid 1", cidVer: 1, wantErr: false},
82 {name: "invalid 2", cidVer: 2, wantErr: true, errMsg: "must be 0 or 1"},
83 {name: "invalid -1", cidVer: -1, wantErr: true, errMsg: "must be 0 or 1"},
84 {name: "invalid 100", cidVer: 100, wantErr: true, errMsg: "must be 0 or 1"},
85 }
86
87 for _, tt := range tests {
88 t.Run(tt.name, func(t *testing.T) {
89 cfg := &Import{
90 CidVersion: *NewOptionalInteger(tt.cidVer),
91 }
92
93 err := ValidateImportConfig(cfg)
94
95 if tt.wantErr {
96 if err == nil {
97 t.Errorf("ValidateImportConfig() expected error for cidVer=%d, got nil", tt.cidVer)
98 } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
99 t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
100 }
101 } else {
102 if err != nil {
103 t.Errorf("ValidateImportConfig() unexpected error for cidVer=%d: %v", tt.cidVer, err)
104 }
105 }
106 })
107 }
108 }
109
110 func TestValidateImportConfig_UnixFSFileMaxLinks(t *testing.T) {
111 tests := []struct {
112 name string
113 maxLinks int64
114 wantErr bool
115 errMsg string
116 }{
117 {name: "valid 1", maxLinks: 1, wantErr: false},
118 {name: "valid 174", maxLinks: 174, wantErr: false},
119 {name: "valid 1000", maxLinks: 1000, wantErr: false},
120 {name: "invalid 0", maxLinks: 0, wantErr: true, errMsg: "must be positive"},
121 {name: "invalid -1", maxLinks: -1, wantErr: true, errMsg: "must be positive"},
122 }
123
124 for _, tt := range tests {
125 t.Run(tt.name, func(t *testing.T) {
126 cfg := &Import{
127 UnixFSFileMaxLinks: *NewOptionalInteger(tt.maxLinks),
128 }
129
130 err := ValidateImportConfig(cfg)
131
132 if tt.wantErr {
133 if err == nil {
134 t.Errorf("ValidateImportConfig() expected error for maxLinks=%d, got nil", tt.maxLinks)
135 } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
136 t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
137 }
138 } else {
139 if err != nil {
140 t.Errorf("ValidateImportConfig() unexpected error for maxLinks=%d: %v", tt.maxLinks, err)
141 }
142 }
143 })
144 }
145 }
146
147 func TestValidateImportConfig_UnixFSDirectoryMaxLinks(t *testing.T) {
148 tests := []struct {
149 name string
150 maxLinks int64
151 wantErr bool
152 errMsg string
153 }{
154 {name: "valid 0", maxLinks: 0, wantErr: false}, // 0 means no limit
155 {name: "valid 1", maxLinks: 1, wantErr: false},
156 {name: "valid 1000", maxLinks: 1000, wantErr: false},
157 {name: "invalid -1", maxLinks: -1, wantErr: true, errMsg: "must be non-negative"},
158 {name: "invalid -100", maxLinks: -100, wantErr: true, errMsg: "must be non-negative"},
159 }
160
161 for _, tt := range tests {
162 t.Run(tt.name, func(t *testing.T) {
163 cfg := &Import{
164 UnixFSDirectoryMaxLinks: *NewOptionalInteger(tt.maxLinks),
165 }
166
167 err := ValidateImportConfig(cfg)
168
169 if tt.wantErr {
170 if err == nil {
171 t.Errorf("ValidateImportConfig() expected error for maxLinks=%d, got nil", tt.maxLinks)
172 } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
173 t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
174 }
175 } else {
176 if err != nil {
177 t.Errorf("ValidateImportConfig() unexpected error for maxLinks=%d: %v", tt.maxLinks, err)
178 }
179 }
180 })
181 }
182 }
183
184 func TestValidateImportConfig_BatchMax(t *testing.T) {
185 tests := []struct {
186 name string
187 maxNodes int64
188 maxSize int64
189 wantErr bool
190 errMsg string
191 }{
192 {name: "valid nodes 1", maxNodes: 1, maxSize: -999, wantErr: false},
193 {name: "valid nodes 128", maxNodes: 128, maxSize: -999, wantErr: false},
194 {name: "valid size 1", maxNodes: -999, maxSize: 1, wantErr: false},
195 {name: "valid size 20MB", maxNodes: -999, maxSize: 20 << 20, wantErr: false},
196 {name: "invalid nodes 0", maxNodes: 0, maxSize: -999, wantErr: true, errMsg: "BatchMaxNodes must be positive"},
197 {name: "invalid nodes -1", maxNodes: -1, maxSize: -999, wantErr: true, errMsg: "BatchMaxNodes must be positive"},
198 {name: "invalid size 0", maxNodes: -999, maxSize: 0, wantErr: true, errMsg: "BatchMaxSize must be positive"},
199 {name: "invalid size -1", maxNodes: -999, maxSize: -1, wantErr: true, errMsg: "BatchMaxSize must be positive"},
200 }
201
202 for _, tt := range tests {
203 t.Run(tt.name, func(t *testing.T) {
204 cfg := &Import{}
205 if tt.maxNodes != -999 {
206 cfg.BatchMaxNodes = *NewOptionalInteger(tt.maxNodes)
207 }
208 if tt.maxSize != -999 {
209 cfg.BatchMaxSize = *NewOptionalInteger(tt.maxSize)
210 }
211
212 err := ValidateImportConfig(cfg)
213
214 if tt.wantErr {
215 if err == nil {
216 t.Errorf("ValidateImportConfig() expected error, got nil")
217 } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
218 t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
219 }
220 } else {
221 if err != nil {
222 t.Errorf("ValidateImportConfig() unexpected error: %v", err)
223 }
224 }
225 })
226 }
227 }
228
229 func TestValidateImportConfig_UnixFSChunker(t *testing.T) {
230 tests := []struct {
231 name string
232 chunker string
233 wantErr bool
234 errMsg string
235 }{
236 {name: "valid size-262144", chunker: "size-262144", wantErr: false},
237 {name: "valid size-1", chunker: "size-1", wantErr: false},
238 {name: "valid size-1048576", chunker: "size-1048576", wantErr: false},
239 {name: "valid rabin", chunker: "rabin-128-256-512", wantErr: false},
240 {name: "valid rabin min", chunker: "rabin-16-32-64", wantErr: false},
241 {name: "valid buzhash", chunker: "buzhash", wantErr: false},
242 {name: "invalid size-", chunker: "size-", wantErr: true, errMsg: "invalid format"},
243 {name: "invalid size-abc", chunker: "size-abc", wantErr: true, errMsg: "invalid format"},
244 {name: "invalid rabin-", chunker: "rabin-", wantErr: true, errMsg: "invalid format"},
245 {name: "invalid rabin-128", chunker: "rabin-128", wantErr: true, errMsg: "invalid format"},
246 {name: "invalid rabin-128-256", chunker: "rabin-128-256", wantErr: true, errMsg: "invalid format"},
247 {name: "invalid rabin-a-b-c", chunker: "rabin-a-b-c", wantErr: true, errMsg: "invalid format"},
248 {name: "invalid unknown", chunker: "unknown", wantErr: true, errMsg: "invalid format"},
249 {name: "invalid empty", chunker: "", wantErr: true, errMsg: "invalid format"},
250 }
251
252 for _, tt := range tests {
253 t.Run(tt.name, func(t *testing.T) {
254 cfg := &Import{
255 UnixFSChunker: *NewOptionalString(tt.chunker),
256 }
257
258 err := ValidateImportConfig(cfg)
259
260 if tt.wantErr {
261 if err == nil {
262 t.Errorf("ValidateImportConfig() expected error for chunker=%s, got nil", tt.chunker)
263 } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
264 t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
265 }
266 } else {
267 if err != nil {
268 t.Errorf("ValidateImportConfig() unexpected error for chunker=%s: %v", tt.chunker, err)
269 }
270 }
271 })
272 }
273 }
274
275 func TestValidateImportConfig_HashFunction(t *testing.T) {
276 tests := []struct {
277 name string
278 hashFunc string
279 wantErr bool
280 errMsg string
281 }{
282 {name: "valid sha2-256", hashFunc: "sha2-256", wantErr: false},
283 {name: "valid sha2-512", hashFunc: "sha2-512", wantErr: false},
284 {name: "valid sha3-256", hashFunc: "sha3-256", wantErr: false},
285 {name: "valid blake2b-256", hashFunc: "blake2b-256", wantErr: false},
286 {name: "valid blake3", hashFunc: "blake3", wantErr: false},
287 {name: "invalid unknown", hashFunc: "unknown-hash", wantErr: true, errMsg: "unrecognized"},
288 {name: "invalid empty", hashFunc: "", wantErr: true, errMsg: "unrecognized"},
289 }
290
291 // Check for hashes that exist but are not allowed
292 // MD5 should exist but not be allowed
293 if code, ok := mh.Names["md5"]; ok {
294 tests = append(tests, struct {
295 name string
296 hashFunc string
297 wantErr bool
298 errMsg string
299 }{name: "md5 not allowed", hashFunc: "md5", wantErr: true, errMsg: "not allowed"})
300 _ = code // use the variable
301 }
302
303 for _, tt := range tests {
304 t.Run(tt.name, func(t *testing.T) {
305 cfg := &Import{
306 HashFunction: *NewOptionalString(tt.hashFunc),
307 }
308
309 err := ValidateImportConfig(cfg)
310
311 if tt.wantErr {
312 if err == nil {
313 t.Errorf("ValidateImportConfig() expected error for hashFunc=%s, got nil", tt.hashFunc)
314 } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
315 t.Errorf("ValidateImportConfig() error = %v, want error containing %q", err, tt.errMsg)
316 }
317 } else {
318 if err != nil {
319 t.Errorf("ValidateImportConfig() unexpected error for hashFunc=%s: %v", tt.hashFunc, err)
320 }
321 }
322 })
323 }
324 }
325
326 func TestValidateImportConfig_DefaultValue(t *testing.T) {
327 // Test that default (unset) value doesn't trigger validation
328 cfg := &Import{}
329
330 err := ValidateImportConfig(cfg)
331 if err != nil {
332 t.Errorf("ValidateImportConfig() unexpected error for default config: %v", err)
333 }
334 }
335
336 func TestIsValidChunker(t *testing.T) {
337 tests := []struct {
338 chunker string
339 want bool
340 }{
341 {"buzhash", true},
342 {"size-262144", true},
343 {"size-1", true},
344 {"size-0", false}, // 0 is not valid - must be positive
345 {"size-9999999", true},
346 {"rabin-128-256-512", true},
347 {"rabin-16-32-64", true},
348 {"rabin-1-2-3", true},
349 {"rabin-512-256-128", false}, // Invalid ordering: min > avg > max
350 {"rabin-256-128-512", false}, // Invalid ordering: min > avg
351 {"rabin-128-512-256", false}, // Invalid ordering: avg > max
352
353 {"", false},
354 {"size-", false},
355 {"size-abc", false},
356 {"size--1", false},
357 {"rabin-", false},
358 {"rabin-128", false},
359 {"rabin-128-256", false},
360 {"rabin-128-256-512-1024", false},
361 {"rabin-a-b-c", false},
362 {"unknown", false},
363 {"buzzhash", false}, // typo
364 }
365
366 for _, tt := range tests {
367 t.Run(tt.chunker, func(t *testing.T) {
368 if got := isValidChunker(tt.chunker); got != tt.want {
369 t.Errorf("isValidChunker(%q) = %v, want %v", tt.chunker, got, tt.want)
370 }
371 })
372 }
373 }
374
375 func TestIsPowerOfTwo(t *testing.T) {
376 tests := []struct {
377 n int64
378 want bool
379 }{
380 {0, false},
381 {1, true},
382 {2, true},
383 {3, false},
384 {4, true},
385 {5, false},
386 {6, false},
387 {7, false},
388 {8, true},
389 {16, true},
390 {32, true},
391 {64, true},
392 {100, false},
393 {128, true},
394 {256, true},
395 {512, true},
396 {1024, true},
397 {2048, true},
398 {-1, false},
399 {-8, false},
400 }
401
402 for _, tt := range tests {
403 t.Run("", func(t *testing.T) {
404 if got := isPowerOfTwo(tt.n); got != tt.want {
405 t.Errorf("isPowerOfTwo(%d) = %v, want %v", tt.n, got, tt.want)
406 }
407 })
408 }
409 }
410
411 func TestValidateImportConfig_HAMTSizeEstimation(t *testing.T) {
412 tests := []struct {
413 name string
414 value string
415 wantErr bool
416 errMsg string
417 }{
418 {name: "valid links", value: HAMTSizeEstimationLinks, wantErr: false},
419 {name: "valid block", value: HAMTSizeEstimationBlock, wantErr: false},
420 {name: "valid disabled", value: HAMTSizeEstimationDisabled, wantErr: false},
421 {name: "invalid unknown", value: "unknown", wantErr: true, errMsg: "must be"},
422 {name: "invalid empty", value: "", wantErr: true, errMsg: "must be"},
423 {name: "invalid typo", value: "link", wantErr: true, errMsg: "must be"},
424 }
425
426 for _, tt := range tests {
427 t.Run(tt.name, func(t *testing.T) {
428 cfg := &Import{
429 UnixFSHAMTDirectorySizeEstimation: *NewOptionalString(tt.value),
430 }
431
432 err := ValidateImportConfig(cfg)
433
434 if tt.wantErr {
435 if err == nil {
436 t.Errorf("expected error for value=%q, got nil", tt.value)
437 } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
438 t.Errorf("error = %v, want error containing %q", err, tt.errMsg)
439 }
440 } else {
441 if err != nil {
442 t.Errorf("unexpected error for value=%q: %v", tt.value, err)
443 }
444 }
445 })
446 }
447 }
448
449 func TestValidateImportConfig_DAGLayout(t *testing.T) {
450 tests := []struct {
451 name string
452 value string
453 wantErr bool
454 errMsg string
455 }{
456 {name: "valid balanced", value: DAGLayoutBalanced, wantErr: false},
457 {name: "valid trickle", value: DAGLayoutTrickle, wantErr: false},
458 {name: "invalid unknown", value: "unknown", wantErr: true, errMsg: "must be"},
459 {name: "invalid empty", value: "", wantErr: true, errMsg: "must be"},
460 {name: "invalid flat", value: "flat", wantErr: true, errMsg: "must be"},
461 }
462
463 for _, tt := range tests {
464 t.Run(tt.name, func(t *testing.T) {
465 cfg := &Import{
466 UnixFSDAGLayout: *NewOptionalString(tt.value),
467 }
468
469 err := ValidateImportConfig(cfg)
470
471 if tt.wantErr {
472 if err == nil {
473 t.Errorf("expected error for value=%q, got nil", tt.value)
474 } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
475 t.Errorf("error = %v, want error containing %q", err, tt.errMsg)
476 }
477 } else {
478 if err != nil {
479 t.Errorf("unexpected error for value=%q: %v", tt.value, err)
480 }
481 }
482 })
483 }
484 }
485
486 func TestImport_UnixFSCidBuilder(t *testing.T) {
487 defaultMhType := mh.Names[strings.ToLower(DefaultHashFunction)]
488
489 tests := []struct {
490 name string
491 cfg Import
492 wantCidVer uint64
493 wantMhType uint64
494 }{
495 {
496 name: "CIDv1 explicit",
497 cfg: Import{CidVersion: *NewOptionalInteger(1)},
498 wantCidVer: 1,
499 wantMhType: defaultMhType,
500 },
501 {
502 name: "CIDv0 explicit",
503 cfg: Import{CidVersion: *NewOptionalInteger(0)},
504 wantCidVer: 0,
505 wantMhType: defaultMhType,
506 },
507 {
508 name: "non-default hash upgrades CIDv0 to CIDv1",
509 cfg: Import{HashFunction: *NewOptionalString("sha2-512")},
510 wantCidVer: 1,
511 wantMhType: mh.SHA2_512,
512 },
513 {
514 name: "CIDv1 with sha2-512",
515 cfg: Import{
516 CidVersion: *NewOptionalInteger(1),
517 HashFunction: *NewOptionalString("sha2-512"),
518 },
519 wantCidVer: 1,
520 wantMhType: mh.SHA2_512,
521 },
522 }
523
524 for _, tt := range tests {
525 t.Run(tt.name, func(t *testing.T) {
526 builder, err := tt.cfg.UnixFSCidBuilder()
527 if err != nil {
528 t.Fatalf("unexpected error: %v", err)
529 }
530 if builder == nil {
531 t.Fatal("expected non-nil builder")
532 }
533 c, err := builder.Sum([]byte("test"))
534 if err != nil {
535 t.Fatalf("builder.Sum failed: %v", err)
536 }
537 pref := c.Prefix()
538 if pref.Version != tt.wantCidVer {
539 t.Errorf("CID version = %d, want %d", pref.Version, tt.wantCidVer)
540 }
541 if pref.MhType != tt.wantMhType {
542 t.Errorf("multihash type = 0x%x, want 0x%x", pref.MhType, tt.wantMhType)
543 }
544 })
545 }
546 }
547
548 // TestImport_UnixFSCidBuilderDefaults verifies that UnixFSCidBuilder always
549 // returns an explicit builder even when no config is set, so that MFS
550 // respects kubo's DefaultCidVersion rather than relying on boxo's internal
551 // CIDv0 default (relevant for https://github.com/ipfs/kubo/issues/4143).
552 func TestImport_UnixFSCidBuilderDefaults(t *testing.T) {
553 cfg := &Import{}
554 builder, err := cfg.UnixFSCidBuilder()
555 if err != nil {
556 t.Fatalf("unexpected error: %v", err)
557 }
558 if builder == nil {
559 t.Fatal("expected non-nil builder at defaults")
560 }
561 c, err := builder.Sum([]byte("test"))
562 if err != nil {
563 t.Fatalf("builder.Sum failed: %v", err)
564 }
565 pref := c.Prefix()
566 if pref.Version != uint64(DefaultCidVersion) {
567 t.Errorf("CID version = %d, want DefaultCidVersion (%d)", pref.Version, DefaultCidVersion)
568 }
569 wantMhType := mh.Names[strings.ToLower(DefaultHashFunction)]
570 if pref.MhType != wantMhType {
571 t.Errorf("multihash type = 0x%x, want 0x%x (DefaultHashFunction=%s)", pref.MhType, wantMhType, DefaultHashFunction)
572 }
573 }
574
575 func TestImport_HAMTSizeEstimationMode(t *testing.T) {
576 tests := []struct {
577 cfg string
578 want io.SizeEstimationMode
579 }{
580 {HAMTSizeEstimationLinks, io.SizeEstimationLinks},
581 {HAMTSizeEstimationBlock, io.SizeEstimationBlock},
582 {HAMTSizeEstimationDisabled, io.SizeEstimationDisabled},
583 {"", io.SizeEstimationLinks}, // default (unset returns default)
584 {"unknown", io.SizeEstimationLinks}, // fallback to default
585 }
586
587 for _, tt := range tests {
588 t.Run(tt.cfg, func(t *testing.T) {
589 var imp Import
590 if tt.cfg != "" {
591 imp.UnixFSHAMTDirectorySizeEstimation = *NewOptionalString(tt.cfg)
592 }
593 got := imp.HAMTSizeEstimationMode()
594 if got != tt.want {
595 t.Errorf("Import.HAMTSizeEstimationMode() with %q = %v, want %v", tt.cfg, got, tt.want)
596 }
597 })
598 }
599 }