package main import ( "encoding/json" "os" "path/filepath" "runtime" "strings" "testing" ) func validConfig() Config { config := DefaultConfig() config.Server = "https://devplace.net" config.Secret = "dpn_abcdefghijklmnop" return config } func TestConfigRoundTripShapeAndMode(t *testing.T) { path := filepath.Join(t.TempDir(), "sub", "config.json") if err := SaveConfig(path, validConfig()); err != nil { t.Fatal(err) } raw, _ := os.ReadFile(path) keys := map[string]any{} if err := json.Unmarshal(raw, &keys); err != nil { t.Fatal(err) } for _, key := range []string{"server", "secret", "bandwidth_mb_day", "max_concurrent", "max_tunnels", "active_from", "active_to", "timezone"} { if _, ok := keys[key]; !ok { t.Errorf("missing key %s", key) } } if len(keys) != 8 { t.Errorf("unexpected keys %v", keys) } if runtime.GOOS != "windows" { info, _ := os.Stat(path) if info.Mode().Perm() != 0o600 { t.Errorf("mode %v", info.Mode().Perm()) } } loaded, err := LoadConfig(path) if err != nil || loaded != validConfig() { t.Fatalf("loaded %+v %v", loaded, err) } } func TestConfigValidationTable(t *testing.T) { cases := map[string]func(*Config){ "server scheme": func(c *Config) { c.Server = "ftp://x" }, "server empty": func(c *Config) { c.Server = "" }, "secret prefix": func(c *Config) { c.Secret = "abc_1234567890" }, "secret short": func(c *Config) { c.Secret = "dpn_" }, "bandwidth low": func(c *Config) { c.BandwidthMBDay = 0 }, "bandwidth high": func(c *Config) { c.BandwidthMBDay = MaxDonorMBPerDay + 1 }, "concurrency": func(c *Config) { c.MaxConcurrent = 99 }, "tunnels": func(c *Config) { c.MaxTunnels = 0 }, "hours format": func(c *Config) { c.ActiveFrom, c.ActiveTo = "9:00", "17:00" }, "hours lone": func(c *Config) { c.ActiveFrom = "09:00" }, "timezone": func(c *Config) { c.Timezone = "Mars/Base" }, } for name, mutate := range cases { config := validConfig() mutate(&config) if config.Validate() == nil { t.Errorf("%s accepted", name) } } good := validConfig() good.ActiveFrom, good.ActiveTo, good.Timezone = "22:00", "06:00", "Europe/Amsterdam" if err := good.Validate(); err != nil { t.Errorf("good config: %v", err) } } func TestLoadRejectsUnknownFieldsAndMissingFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.json") if _, err := LoadConfig(path); err == nil || !strings.Contains(err.Error(), "login") { t.Errorf("missing file: %v", err) } _ = os.WriteFile(path, []byte(`{"server":"https://x.y","secret":"dpn_abcdefghijk","surprise":1}`), 0o600) if _, err := LoadConfig(path); err == nil { t.Error("unknown field accepted") } } func TestCommandsVersionAndUsage(t *testing.T) { var out, errOut strings.Builder if code := execute([]string{"version"}, &out, &errOut); code != exitOK || !strings.Contains(out.String(), Version) { t.Fatalf("version: %d %q", code, out.String()) } if code := execute(nil, &out, &errOut); code != exitUsage { t.Fatalf("no args: %d", code) } if code := execute([]string{"bogus"}, &out, &errOut); code != exitUsage { t.Fatalf("bogus: %d", code) } } func TestLoginStoresWithoutVerifyAndNeverEchoesSecret(t *testing.T) { path := filepath.Join(t.TempDir(), "config.json") var out, errOut strings.Builder args := []string{"--config", path, "--server", "https://devplace.net", "--secret", "dpn_abcdefghijklmnop", "--no-verify"} if code := execute(append([]string{"login"}, args...), &out, &errOut); code != exitOK { t.Fatalf("login: %d %s", code, errOut.String()) } if strings.Contains(out.String()+errOut.String(), "dpn_abcdefghijklmnop") { t.Fatal("secret was printed") } out.Reset() if code := execute([]string{"login", "--config", path, "--server", "nope", "--secret", "x"}, &out, &errOut); code != exitUsage { t.Fatalf("bad login accepted: %d", code) } }