package cli import ( "path/filepath" "testing" ) func TestParseArgs(t *testing.T) { tests := []struct { name string args []string action Action wantVal map[string]string uris []string }{ {"short attached", []string{"-x16", "https://e.com"}, Run, map[string]string{"max-connection-per-server": "16"}, []string{"https://e.com"}}, {"long equals + bool", []string{"--split=8", "-c", "u"}, Run, map[string]string{"split": "8", "continue": "true"}, []string{"u"}}, {"long separate value", []string{"--out", "f.bin", "u"}, Run, map[string]string{"out": "f.bin"}, []string{"u"}}, {"bool bundle", []string{"-cq", "u"}, Run, map[string]string{"continue": "true", "quiet": "true"}, []string{"u"}}, {"repeatable header", []string{"--header", "A: 1", "--header", "B: 2", "u"}, Run, map[string]string{"header": "A: 1\nB: 2"}, []string{"u"}}, {"two uris", []string{"a", "b"}, Run, map[string]string{}, []string{"a", "b"}}, {"permute flag after uri", []string{"u", "--split=8"}, Run, map[string]string{"split": "8"}, []string{"u"}}, {"permute interleaved", []string{"a", "-c", "b", "--quiet"}, Run, map[string]string{"continue": "true", "quiet": "true"}, []string{"a", "b"}}, {"dash dash terminator", []string{"--split=8", "--", "--not-a-flag", "u"}, Run, map[string]string{"split": "8"}, []string{"--not-a-flag", "u"}}, {"long int + magnet uri", []string{"--bt-stop-timeout=120", "magnet:x"}, Run, map[string]string{"bt-stop-timeout": "120"}, []string{"magnet:x"}}, {"help", []string{"--help"}, ShowHelp, nil, nil}, {"help short", []string{"-h"}, ShowHelp, nil, nil}, {"version", []string{"-v"}, ShowVersion, nil, nil}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { vals, uris, action, _, err := parseArgs(tc.args) if err != nil { t.Fatalf("unexpected error: %v", err) } if action != tc.action { t.Fatalf("action = %v, want %v", action, tc.action) } if tc.action != Run { return } for k, want := range tc.wantVal { if vals[k] != want { t.Errorf("vals[%q] = %q, want %q", k, vals[k], want) } } if len(uris) != len(tc.uris) { t.Fatalf("uris = %v, want %v", uris, tc.uris) } for i := range uris { if uris[i] != tc.uris[i] { t.Errorf("uris[%d] = %q, want %q", i, uris[i], tc.uris[i]) } } }) } } func TestParseArgsErrors(t *testing.T) { for _, args := range [][]string{ {"--unknown-flag"}, {"-Q"}, // -Q is not a defined short flag {"--out"}, // missing value } { if _, _, _, _, err := parseArgs(args); err == nil { t.Errorf("parseArgs(%v) = nil error, want error", args) } } } func TestParseSize(t *testing.T) { tests := []struct { in string want int64 }{ {"", 0}, {"100", 100}, {"512K", 512 << 10}, {"20M", 20 << 20}, {"1024M", 1 << 30}, // there is no G unit; the 1G bound is typed as 1024M {"1Mi", 1 << 20}, // the first K/M is taken and the rest discarded {"10MB", 10 << 20}, {"2k", 2 << 10}, } for _, tc := range tests { got, err := parseSize(tc.in) if err != nil { t.Errorf("parseSize(%q) error: %v", tc.in, err) continue } if got != tc.want { t.Errorf("parseSize(%q) = %d, want %d", tc.in, got, tc.want) } } } func TestValidateRange(t *testing.T) { x := byLong["max-connection-per-server"] if err := validate(x, "16"); err != nil { t.Errorf("validate 16: %v", err) } if err := validate(x, "99"); err == nil { t.Errorf("validate 99: want out-of-range error") } a := byLong["file-allocation"] if err := validate(a, "bogus"); err == nil { t.Errorf("validate enum bogus: want error") } } func TestValidateIntFloor(t *testing.T) { // Min:0 now enforces a floor of 0: negatives are rejected. m := byLong["max-tries"] // Min 0, "0 = unlimited" runtime meaning preserved if err := validate(m, "0"); err != nil { t.Errorf("validate max-tries 0: %v, want nil (0 stays a valid input)", err) } if err := validate(m, "-1"); err == nil { t.Errorf("validate max-tries -1: want below-minimum error") } } func TestValidateFloatNonNegative(t *testing.T) { // Float options (seed-time, seed-ratio) reject negatives: a negative // SeedTime would fire the seed timer immediately and a negative SeedRatio // is silently ignored, so neither is a meaningful input. for _, name := range []string{"seed-ratio", "seed-time"} { o := byLong[name] if err := validate(o, "-1"); err == nil { t.Errorf("validate %s -1: want below-minimum error", name) } if err := validate(o, "0"); err != nil { t.Errorf("validate %s 0: %v, want nil", name, err) } if err := validate(o, "1.5"); err != nil { t.Errorf("validate %s 1.5: %v, want nil", name, err) } } } func TestValidateBool(t *testing.T) { b := byLong["enable-dht"] // Only literal true/false are accepted (case-sensitive), with surrounding // whitespace trimmed. for _, ok := range []string{"true", "false", " true ", "false\t"} { if err := validate(b, ok); err != nil { t.Errorf("validate bool %q: %v", ok, err) } } // The invented spellings and any case variant must now fail. for _, bad := range []string{"yes", "no", "1", "0", "on", "off", "flase", "True", "TRUE", "tRuE", "False", "FALSE"} { if err := validate(b, bad); err == nil { t.Errorf("validate bool %q: want error (only true/false accepted)", bad) } } } func TestValidateSizeBounds(t *testing.T) { k := byLong["min-split-size"] // 1M..1024M (no G unit) if err := validate(k, "20M"); err != nil { t.Errorf("validate min-split-size 20M: %v", err) } if err := validate(k, "512K"); err == nil { t.Errorf("validate min-split-size 512K: want below-minimum error") } // 1025M exceeds the 1<<30 cap and still parses, so it tests the bounds path // (where "2G" would have failed earlier as an unknown unit). if err := validate(k, "1025M"); err == nil { t.Errorf("validate min-split-size 1025M: want above-maximum error") } } func TestParseSizeNegative(t *testing.T) { if _, err := parseSize("-1"); err == nil { t.Errorf("parseSize(-1): want error (size parsing rejects negative values)") } if _, err := parseSize("-5M"); err == nil { t.Errorf("parseSize(-5M): want error") } } // size parsing knows only K and M; a "G" suffix is not a unit and must be // rejected rather than silently treated as 2^30. func TestParseSizeNoGigaUnit(t *testing.T) { for _, in := range []string{"1G", "1g", "1Gi", "5GB"} { if _, err := parseSize(in); err == nil { t.Errorf("parseSize(%q): want error (no G unit)", in) } } } func TestExplicitConfMissingFatal(t *testing.T) { missing := filepath.Join(t.TempDir(), "no-such-file.conf") // Default (non-explicit) missing path is silently ignored. if err := applyConfig(newOptions(), missing, false); err != nil { t.Errorf("applyConfig default missing: %v, want nil", err) } // Explicit missing path is fatal. if err := applyConfig(newOptions(), missing, true); err == nil { t.Errorf("applyConfig explicit missing: want error") } } func TestOptionsGetters(t *testing.T) { o := newOptions() o.vals["split"] = "8" o.vals["min-split-size"] = "20M" o.vals["continue"] = "true" o.vals["seed-ratio"] = "1.5" if o.Int("split") != 8 { t.Errorf("Int split = %d", o.Int("split")) } if o.Size("min-split-size") != 20<<20 { t.Errorf("Size min-split-size = %d", o.Size("min-split-size")) } if !o.Bool("continue") { t.Errorf("Bool continue = false") } if o.Float("seed-ratio") != 1.5 { t.Errorf("Float seed-ratio = %v", o.Float("seed-ratio")) } }