Files
2026-09-13 12:38:22 +02:00

320 lines
9.2 KiB
Go

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 source link is a URL someone will set by hand on a fork, so a value that
// would render a broken link has to be refused at startup rather than shipped
// to every page.
func TestSourceURLMustBeAbsoluteOrEmpty(t *testing.T) {
base := func() Config {
c := Config{}
fs := NewSet("test", "UNCENSORED_SEND_TEST_")
c.Register(fs)
if err := fs.Parse(nil); err != nil {
t.Fatal(err)
}
return c
}
c := base()
if err := c.Normalise(); err != nil {
t.Fatalf("the default source URL was refused: %v", err)
}
if c.SourceURL == "" {
t.Error("the default build links no source at all")
}
c = base()
c.SourceURL = ""
if err := c.Normalise(); err != nil {
t.Errorf("switching the source link off was refused: %v", err)
}
for _, bad := range []string{"not a url", "/relative/path", "example.org/repo"} {
c = base()
c.SourceURL = bad
if err := c.Normalise(); err == nil {
t.Errorf("--source-url %q was accepted", bad)
}
}
}
// 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)
}
}
}