package config import ( "fmt" "os" "path/filepath" "strings" "testing" "github.com/cristalhq/aconfig" "github.com/cristalhq/aconfig/aconfigtoml" ) func TestLoadExampleSowerConfigFiles(t *testing.T) { t.Parallel() tests := []struct { name string file string }{ {name: "toml", file: "sower.toml"}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() var cfg SowerConfig if err := loadConfigFileForTest(tt.file, &cfg); err != nil { t.Fatalf("load config file: %v", err) } if err := cfg.Validate(); err != nil { t.Fatalf("validate config: %v", err) } if cfg.Remote.TLS.ServerName != "" { t.Fatalf("unexpected remote tls server name: %q", cfg.Remote.TLS.ServerName) } if cfg.DNS.Serve != "127.0.0.1" { t.Fatalf("unexpected dns serve: %q", cfg.DNS.Serve) } if cfg.DNS.ServeIface != "" { t.Fatalf("unexpected dns serve iface: %q", cfg.DNS.ServeIface) } if cfg.Socks5.Addr != "127.0.0.1:1080" { t.Fatalf("unexpected socks5 addr: %q", cfg.Socks5.Addr) } if cfg.Router.Block.FilePrefix != "**." { t.Fatalf("unexpected block file prefix: %q", cfg.Router.Block.FilePrefix) } }) } } func TestLoadExampleSowerdConfigFiles(t *testing.T) { t.Parallel() tests := []struct { name string content string ext string wantRoutes int }{ { name: "embedded toml", content: strings.ReplaceAll( ExampleSowerdConfigTOML, `fake_site = "/var/www"`, `fake_site = "127.0.0.1:80"`, ), ext: ".toml", }, { name: "file toml", content: strings.ReplaceAll( mustReadFileForTest(t, "sowerd.toml"), `fake_site = "/var/www"`, `fake_site = "127.0.0.1:80"`, ), ext: ".toml", }, { name: "active site routes toml", content: `log_level = "info" serve_ip = "0.0.0.0" password = "change_me" fake_site = "127.0.0.1:80" [[site_routes]] domains = ["a.example.com", "b.example.com"] upstream = "http://127.0.0.1:8080" [[site_routes]] domains = ["c.example.com"] upstream = "https://backend.example.com" `, ext: ".toml", wantRoutes: 2, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "sowerd"+tt.ext) if err := os.WriteFile(path, []byte(tt.content), 0o644); err != nil { t.Fatalf("write temp config: %v", err) } var cfg SowerdConfig if err := loadConfigFileForTest(path, &cfg); err != nil { t.Fatalf("load config file: %v", err) } if err := cfg.Validate(); err != nil { t.Fatalf("validate config: %v", err) } if cfg.ServeIP != "0.0.0.0" { t.Fatalf("unexpected serve ip: %q", cfg.ServeIP) } if cfg.FakeSite != "127.0.0.1:80" { t.Fatalf("unexpected fake site: %q", cfg.FakeSite) } if len(cfg.SiteRoutes) != tt.wantRoutes { t.Fatalf("unexpected site route count: %d", len(cfg.SiteRoutes)) } }) } } // TestExampleSowerdConfigTOMLMatchesFile pins the embedded example to the // checked-in file byte for byte. They are the same artifact in two places: // the constant is what the installer writes to /etc/sower/sowerd.toml, and // the file is what `-c config/sowerd.toml` loads during development. Let // them drift and operators get guidance that contradicts shipped behavior. func TestExampleSowerdConfigTOMLMatchesFile(t *testing.T) { t.Parallel() file := mustReadFileForTest(t, "sowerd.toml") if ExampleSowerdConfigTOML != file { t.Fatalf("ExampleSowerdConfigTOML and config/sowerd.toml have drifted:\n%s", exampleConfigDiff(ExampleSowerdConfigTOML, file)) } } func exampleConfigDiff(embedded, file string) string { embeddedLines := strings.Split(strings.TrimRight(embedded, "\n"), "\n") fileLines := strings.Split(strings.TrimRight(file, "\n"), "\n") var b strings.Builder fmt.Fprintf(&b, " const has %d lines, file has %d lines\n", len(embeddedLines), len(fileLines)) for i := 0; i < len(embeddedLines) || i < len(fileLines); i++ { var e, f string if i < len(embeddedLines) { e = embeddedLines[i] } if i < len(fileLines) { f = fileLines[i] } if e != f { fmt.Fprintf(&b, " line %d\n const: %q\n file: %q\n", i+1, e, f) } } return b.String() } func loadConfigFileForTest(path string, dst any) error { return aconfig.LoaderFor(dst, aconfig.Config{ SkipEnv: true, SkipFlags: true, AllowUnknownFields: false, Files: []string{path}, FileDecoders: map[string]aconfig.FileDecoder{ ".toml": aconfigtoml.New(), }, }).Load() } func mustReadFileForTest(t *testing.T, path string) string { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("read file %s: %v", path, err) } return string(data) }