package config import ( "strings" "testing" "time" ) func TestParseSize(t *testing.T) { cases := map[string]int64{ "": Unlimited, "0": Unlimited, "unlimited": Unlimited, "512": 512, "512B": 512, "2GiB": 2 << 30, "2 GiB": 2 << 30, "2G": 2 << 30, "500MB": 500_000_000, "1.5MiB": 1572864, "2gib": 2 << 30, } for in, want := range cases { got, err := ParseSize(in) if err != nil { t.Errorf("ParseSize(%q): %v", in, err) continue } if got != want { t.Errorf("ParseSize(%q) = %d, want %d", in, got, want) } } for _, in := range []string{"-1", "lots", "2XiB", "GiB2"} { if _, err := ParseSize(in); err == nil { t.Errorf("ParseSize(%q) accepted an invalid size", in) } } } func TestParseDuration(t *testing.T) { cases := map[string]time.Duration{ "": Unlimited, "never": Unlimited, "0": Unlimited, "90m": 90 * time.Minute, "72h": 72 * time.Hour, "3d": 72 * time.Hour, "1w": 168 * time.Hour, "1.5d": 36 * time.Hour, } for in, want := range cases { got, err := ParseDuration(in) if err != nil { t.Errorf("ParseDuration(%q): %v", in, err) continue } if got != want { t.Errorf("ParseDuration(%q) = %s, want %s", in, got, want) } } for _, in := range []string{"-3d", "soon", "3days"} { if _, err := ParseDuration(in); err == nil { t.Errorf("ParseDuration(%q) accepted an invalid duration", in) } } } // The formatters are human-facing and round to two decimals, so they only // round-trip exactly for values that land on a unit boundary. Nothing in the // program re-parses their output; this pins the cases that are shown most. func TestFormatRoundTrips(t *testing.T) { for _, n := range []int64{0, 512, 2 << 30, 100 << 20, 1536} { if got, err := ParseSize(FormatSize(n)); err != nil || got != n { t.Errorf("FormatSize(%d) = %q, which parses back to %d (%v)", n, FormatSize(n), got, err) } } for _, d := range []time.Duration{0, time.Hour, 72 * time.Hour, 90 * time.Minute} { if got, err := ParseDuration(FormatDuration(d)); err != nil || got != d { t.Errorf("FormatDuration(%s) = %q, which parses back to %s (%v)", d, FormatDuration(d), got, err) } // FormatLifetime is shown next to the field a caller types into, so // what it prints has to be something ParseDuration would accept. if got, err := ParseDuration(FormatLifetime(d)); err != nil || got != d { t.Errorf("FormatLifetime(%s) = %q, which parses back to %s (%v)", d, FormatLifetime(d), got, err) } } if got := FormatLifetime(Unlimited); got != "unlimited" { t.Errorf("FormatLifetime(Unlimited) = %q, want %q", got, "unlimited") } } // The spellings themselves, not just their round trip: Duration.String() pads // out the units below the largest one, and trimming that tail is easy to get // wrong in a way that eats a real digit ("1m30s" must not become "1m3"). func TestFormatDurationSpelling(t *testing.T) { for _, c := range []struct { in time.Duration want string }{ {Unlimited, "never"}, {30 * time.Second, "30s"}, {time.Minute, "1m"}, {90 * time.Second, "1m30s"}, {90 * time.Minute, "1h30m"}, {2 * time.Hour, "2h"}, {25 * time.Hour, "25h"}, {72 * time.Hour, "3d"}, } { if got := FormatDuration(c.in); got != c.want { t.Errorf("FormatDuration(%s) = %q, want %q", c.in, got, c.want) } } } // A word the parser does not expect has to be refused, not ignored: the flag // package stops at the first non-flag argument, so a command that accepted one // would silently drop every option after it - including the one naming the // file it writes. func TestParseRefusesStrayArguments(t *testing.T) { newSet := func() (*Set, *string, *bool) { var listen string var flagged bool s := NewSet("test", "UNCENSORED_SEND_TEST_") s.String(&listen, "listen", "l", "default", "ADDR", "listen address") s.Bool(&flagged, "vanity", "", false, "a boolean") return s, &listen, &flagged } for _, args := range [][]string{ {"--vanity", "false"}, // the value a boolean never takes {"stray"}, // a word on its own {"--vanity", "false", "-l", "here"}, // options after the stray word } { s, listen, _ := newSet() err := s.Parse(args) if err == nil { t.Errorf("Parse(%q) was accepted; listen = %q", args, *listen) continue } if !strings.Contains(err.Error(), "unexpected argument") { t.Errorf("Parse(%q) failed with %v, which does not name the problem", args, err) } } // The forms that are actually correct still parse. for _, args := range [][]string{ {"--vanity=false"}, {"--vanity", "-l", "here"}, {"-l", "here"}, {}, } { s, _, _ := newSet() if err := s.Parse(args); err != nil { t.Errorf("Parse(%q) = %v, want it accepted", args, err) } } } // The convention is: one hyphen for a letter, two for a word. It is enforced // here because the stdlib flag package treats both forms as the same. func TestHyphenConvention(t *testing.T) { newSet := func() (*Set, *string) { var listen string s := NewSet("test", "UNCENSORED_SEND_TEST_") s.String(&listen, "listen", "l", "default", "ADDR", "listen address") return s, &listen } for _, args := range [][]string{{"--listen", "x"}, {"-l", "x"}, {"--listen=x"}, {"-l=x"}} { s, listen := newSet() if err := s.Parse(args); err != nil { t.Errorf("%v: %v", args, err) continue } if *listen != "x" { t.Errorf("%v: listen = %q", args, *listen) } } for _, args := range [][]string{{"-listen", "x"}, {"--l", "x"}, {"-listen=x"}} { s, _ := newSet() err := s.Parse(args) if err == nil { t.Errorf("%v was accepted; it breaks the hyphen convention", args) continue } if !strings.Contains(err.Error(), "hyphen") { t.Errorf("%v: error does not explain the convention: %v", args, err) } } } func TestEnvFallback(t *testing.T) { t.Setenv("UNCENSORED_SEND_TEST_MAX_SIZE", "4GiB") var size int64 s := NewSet("test", "UNCENSORED_SEND_TEST_") s.Size(&size, "max-size", "s", "2GiB", "cap") if err := s.Parse(nil); err != nil { t.Fatal(err) } if size != 4<<30 { t.Errorf("size = %d, want 4GiB from the environment", size) } // An explicit flag still wins over the environment. s = NewSet("test", "UNCENSORED_SEND_TEST_") s.Size(&size, "max-size", "s", "2GiB", "cap") if err := s.Parse([]string{"--max-size", "1GiB"}); err != nil { t.Fatal(err) } if size != 1<<30 { t.Errorf("size = %d, want the flag to win", size) } } func TestNormalisePath(t *testing.T) { cases := map[string]string{ "": "/", "/": "/", "send": "/send/", "/send": "/send/", "/send/": "/send/", "a/b": "/a/b/", } for in, want := range cases { if got := NormalisePath(in); got != want { t.Errorf("NormalisePath(%q) = %q, want %q", in, got, want) } } } func TestDefaultExpiryMustFitWithinMax(t *testing.T) { c := &Config{BasePath: "/", DataDir: "d", SweepInterval: time.Minute, MaxConcurrent: 1, MaxExpiry: time.Hour, DefaultExpiry: 24 * time.Hour} if err := c.Normalise(); err == nil { t.Error("a default longer than the maximum was accepted") } c.DefaultExpiry = Unlimited if err := c.Normalise(); err == nil { t.Error("an unlimited default under a finite maximum was accepted") } } func TestPortOverridesListen(t *testing.T) { cases := []struct { listen string port int want string }{ {"127.0.0.1:8080", 0, "127.0.0.1:8080"}, {"127.0.0.1:8080", 9000, "127.0.0.1:9000"}, {"0.0.0.0:8080", 3000, "0.0.0.0:3000"}, {"localhost", 3000, "localhost:3000"}, // a bare host is fine with --port {"[::1]:8080", 3000, "[::1]:3000"}, } for _, c := range cases { cfg := &Config{ Listen: c.listen, Port: c.port, BasePath: "/", DataDir: "d", SweepInterval: time.Minute, MaxConcurrent: 1, } if err := cfg.Normalise(); err != nil { t.Errorf("listen %q port %d: %v", c.listen, c.port, err) continue } if cfg.Listen != c.want { t.Errorf("listen %q port %d => %q, want %q", c.listen, c.port, cfg.Listen, c.want) } } for _, c := range []struct { listen string port int }{{"127.0.0.1:8080", 70000}, {"127.0.0.1:8080", -1}, {"localhost", 0}} { cfg := &Config{ Listen: c.listen, Port: c.port, BasePath: "/", DataDir: "d", SweepInterval: time.Minute, MaxConcurrent: 1, } if err := cfg.Normalise(); err == nil { t.Errorf("listen %q port %d was accepted", c.listen, c.port) } } }