package config_test import ( "fmt" "path/filepath" "strings" "testing" "time" "git.wntrmute.dev/kyle/crossbar/internal/config" ) func TestGoodFile(t *testing.T) { c, err := config.Load(filepath.Join("testdata", "good.toml")) if err != nil { t.Fatalf("Load: %v", err) } if c.Listen != "100.64.0.9:7777" { t.Errorf("Listen = %q", c.Listen) } if c.PollInterval.Duration != 5*time.Second { t.Errorf("PollInterval = %v", c.PollInterval.Duration) } if c.QueueMax != 4 { t.Errorf("QueueMax = %d", c.QueueMax) } alpha := c.Hosts["alpha"] if alpha.BaseURL != "http://alpha.example:11434" { t.Errorf("trailing slash not stripped: %q", alpha.BaseURL) } if alpha.Weight != 2 { t.Errorf("alpha.Weight = %v", alpha.Weight) } if alpha.Models["ornith-1.5-35b-a3b"].Parallel != 4 || alpha.Models["small-9b"].Parallel != 6 { t.Errorf("alpha.Models = %+v", alpha.Models) } beta := c.Hosts["beta"] if beta.Weight != 1 { t.Errorf("beta.Weight default = %v, want 1", beta.Weight) } if beta.Models["ornith-1.5-35b-a3b"].Parallel != 1 { t.Errorf("beta parallel default = %d, want 1", beta.Models["ornith-1.5-35b-a3b"].Parallel) } r := c.Routes["opencode-a"] if len(r.Hosts) != 2 || r.Hosts[0] != "alpha" || r.Hosts[1] != "beta" { t.Errorf("route hosts = %v", r.Hosts) } if r.DefaultModel != "ornith-1.5-35b-a3b" { t.Errorf("DefaultModel = %q", r.DefaultModel) } if c.Routes["hermes-x"].DefaultModel != "" { t.Errorf("hermes-x DefaultModel should be empty") } if !c.Serves("alpha", "small-9b") || c.Serves("beta", "small-9b") || c.Serves("nope", "m") { t.Errorf("Serves is wrong") } } func TestDefaults(t *testing.T) { c, err := config.Parse(strings.NewReader(` listen = "127.0.0.1:1" [hosts.a] base_url = "http://a:1" models = { "m" = { } } [routes.r] hosts = ["a"] `)) if err != nil { t.Fatalf("Parse: %v", err) } if c.PollInterval.Duration != config.DefaultPollInterval { t.Errorf("PollInterval default = %v", c.PollInterval.Duration) } if c.QueueMax != config.DefaultQueueMax { t.Errorf("QueueMax default = %d", c.QueueMax) } } func TestBadFiles(t *testing.T) { cases := []struct{ file, field string }{ {"bad-listen.toml", "listen"}, {"bad-unknown-host.toml", "routes.r.hosts"}, {"bad-default-model.toml", "routes.r.default_model"}, {"bad-unknown-key.toml", "lease_idle"}, } for _, tc := range cases { t.Run(tc.file, func(t *testing.T) { _, err := config.Load(filepath.Join("testdata", tc.file)) if err == nil { t.Fatalf("want error") } e, ok := config.IsError(err) if !ok { t.Fatalf("want *config.Error, got %T: %v", err, err) } if e.Field != tc.field { t.Errorf("Field = %q, want %q (%v)", e.Field, tc.field, err) } if !strings.HasPrefix(err.Error(), "config: "+tc.field+": ") { t.Errorf("Error() = %q", err.Error()) } }) } } func TestBadValues(t *testing.T) { base := ` listen = %q poll_interval = %q [hosts.a] base_url = %q weight = %v models = { "m" = { parallel = %d } } [routes.%s] hosts = ["a"] ` cases := []struct { name string listen, poll, url, route string weight float64 parallel int field string }{ {"empty listen", "", "5s", "http://a:1", "r", 1, 1, "listen"}, {"no port", "127.0.0.1", "5s", "http://a:1", "r", 1, 1, "listen"}, {"v6 any", "[::]:7", "5s", "http://a:1", "r", 1, 1, "listen"}, {"poll too short", "127.0.0.1:7", "500ms", "http://a:1", "r", 1, 1, "poll_interval"}, {"ftp url", "127.0.0.1:7", "5s", "ftp://a:1", "r", 1, 1, "hosts.a.base_url"}, {"no host", "127.0.0.1:7", "5s", "http://", "r", 1, 1, "hosts.a.base_url"}, {"query", "127.0.0.1:7", "5s", "http://a:1/v1?x=1", "r", 1, 1, "hosts.a.base_url"}, {"negative weight", "127.0.0.1:7", "5s", "http://a:1", "r", -1, 1, "hosts.a.weight"}, {"negative parallel", "127.0.0.1:7", "5s", "http://a:1", "r", 1, -2, "hosts.a.models.m.parallel"}, {"route name", "127.0.0.1:7", "5s", "http://a:1", "Bad_Name", 1, 1, "routes.Bad_Name"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { text := fmt.Sprintf(base, tc.listen, tc.poll, tc.url, tc.weight, tc.parallel, tc.route) _, err := config.Parse(strings.NewReader(text)) if err == nil { t.Fatalf("want error for %s", tc.name) } e, ok := config.IsError(err) if !ok { t.Fatalf("want *config.Error, got %T: %v", err, err) } if e.Field != tc.field { t.Errorf("Field = %q, want %q (%v)", e.Field, tc.field, err) } }) } } func TestMissingSections(t *testing.T) { for _, tc := range []struct{ name, text, field string }{ {"no hosts", "listen = \"127.0.0.1:7\"\n[routes.r]\nhosts = [\"a\"]\n", "hosts"}, {"no routes", "listen = \"127.0.0.1:7\"\n[hosts.a]\nbase_url = \"http://a:1\"\nmodels = { \"m\" = { } }\n", "routes"}, {"host without models", "listen = \"127.0.0.1:7\"\n[hosts.a]\nbase_url = \"http://a:1\"\n[routes.r]\nhosts = [\"a\"]\n", "hosts.a.models"}, {"route without hosts", "listen = \"127.0.0.1:7\"\n[hosts.a]\nbase_url = \"http://a:1\"\nmodels = { \"m\" = { } }\n[routes.r]\n", "routes.r.hosts"}, {"host twice", "listen = \"127.0.0.1:7\"\n[hosts.a]\nbase_url = \"http://a:1\"\nmodels = { \"m\" = { } }\n[routes.r]\nhosts = [\"a\", \"a\"]\n", "routes.r.hosts"}, } { t.Run(tc.name, func(t *testing.T) { _, err := config.Parse(strings.NewReader(tc.text)) e, ok := config.IsError(err) if !ok { t.Fatalf("want *config.Error, got %v", err) } if e.Field != tc.field { t.Errorf("Field = %q, want %q", e.Field, tc.field) } }) } } func TestNotTOML(t *testing.T) { _, err := config.Parse(strings.NewReader("listen = [unterminated")) if err == nil { t.Fatal("want error") } if _, ok := config.IsError(err); ok { t.Errorf("a syntax error is not a validation Error") } if !strings.HasPrefix(err.Error(), "config: ") { t.Errorf("Error() = %q", err.Error()) } } func TestMissingFile(t *testing.T) { if _, err := config.Load(filepath.Join("testdata", "does-not-exist.toml")); err == nil { t.Fatal("want error") } }