package client import ( "encoding/json" "errors" "os" "path/filepath" "strings" "testing" ) func TestEnsureConfigWritesEverySetting(t *testing.T) { path := filepath.Join(t.TempDir(), "keeporg", "config.json") c, err := EnsureConfig(path) if err != nil { t.Fatal(err) } if c.PollSeconds != 2 || c.RequestTimeoutSeconds != 60 || !c.Notifications { t.Fatalf("defaults not applied: %+v", c) } b, err := os.ReadFile(path) if err != nil { t.Fatal(err) } var keys map[string]any if err := json.Unmarshal(b, &keys); err != nil { t.Fatal(err) } for _, k := range []string{"server_url", "username", "token", "device_label", "mirror_dir", "poll_seconds", "request_timeout_seconds", "ca_file", "log_file", "notifications"} { if _, ok := keys[k]; !ok { t.Errorf("written config lacks %q", k) } } if fi, _ := os.Stat(path); fi.Mode().Perm() != 0o600 { t.Errorf("config mode %v, want 0600", fi.Mode().Perm()) } } func TestLoadConfigKeepsDefaultsForMissingKeys(t *testing.T) { path := filepath.Join(t.TempDir(), "config.json") // What a 0.4 client wrote: no notifications key, which must not read as // false. old := `{"server_url":"https://k.example.com/","token":"t","username":"u","mirror_dir":"","poll_seconds":0}` if err := os.WriteFile(path, []byte(old), 0o600); err != nil { t.Fatal(err) } c, err := LoadConfig(path) if err != nil { t.Fatal(err) } if !c.Notifications || c.PollSeconds != 2 || c.MirrorDir == "" || c.DeviceLabel == "" { t.Fatalf("defaults lost: %+v", c) } if c.ServerURL != "https://k.example.com" { t.Fatalf("trailing slash kept: %q", c.ServerURL) } } func TestLoadConfigExpandsHome(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Skip(err) } path := filepath.Join(t.TempDir(), "config.json") if err := os.WriteFile(path, []byte(`{"mirror_dir":"~/Vaults","log_file":"~/keeporg.log"}`), 0o600); err != nil { t.Fatal(err) } c, err := LoadConfig(path) if err != nil { t.Fatal(err) } if c.MirrorDir != filepath.Join(home, "Vaults") || c.LogFile != filepath.Join(home, "keeporg.log") { t.Fatalf("~ not expanded: %q %q", c.MirrorDir, c.LogFile) } } func TestLoadConfigRejectsUnusableSettings(t *testing.T) { for name, body := range map[string]string{ "scheme": `{"server_url":"keeporg.example.com"}`, "ca_file": `{"ca_file":"/nonexistent/ca.pem"}`, "syntax": `{"server_url":`, } { path := filepath.Join(t.TempDir(), "config.json") if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } if _, err := LoadConfig(path); err == nil || errors.Is(err, ErrNoConfig) { t.Errorf("%s: want a validation error, got %v", name, err) } } } func TestConfigAPIRejectsEmptyCAFile(t *testing.T) { ca := filepath.Join(t.TempDir(), "ca.pem") if err := os.WriteFile(ca, []byte("not a certificate"), 0o600); err != nil { t.Fatal(err) } c := DefaultConfig() c.ServerURL, c.CAFile = "https://k.example.com", ca if _, err := c.API(); err == nil || !strings.Contains(err.Error(), "no PEM") { t.Fatalf("want a PEM error, got %v", err) } } func TestLockDirAdmitsOneAgent(t *testing.T) { dir := t.TempDir() unlock, err := LockDir(dir) if err != nil { t.Fatal(err) } if _, err := LockDir(dir); !errors.Is(err, ErrMirrorBusy) { t.Fatalf("second lock: want ErrMirrorBusy, got %v", err) } unlock() again, err := LockDir(dir) if err != nil { t.Fatalf("lock after release: %v", err) } again() } func TestCheckVerificationURLStaysOnTheServer(t *testing.T) { ok := []string{"https://k.example.com/device?code=ABCD-EFGH"} bad := []string{ "file:///Applications/Calculator.app", `\\attacker\share\x.exe`, "https://evil.example/device?code=X", "http://k.example.com/device?code=X", "https://user@k.example.com/device", "https://k.example.com/somewhere-else", "keeporg://device-approved", } for _, u := range ok { if err := CheckVerificationURL("https://k.example.com", u); err != nil { t.Errorf("%s refused: %v", u, err) } } for _, u := range bad { if err := CheckVerificationURL("https://k.example.com", u); err == nil { t.Errorf("%s accepted", u) } } } func TestPlainHTTPIsRefusedExceptLocally(t *testing.T) { c := DefaultConfig() for url, wantErr := range map[string]bool{ "http://keeporg.example.com": true, "http://127.0.0.1:8765": false, "http://localhost:8765": false, "https://keeporg.example.com": false, } { c.ServerURL = url if err := c.Validate(); (err != nil) != wantErr { t.Errorf("%s: %v", url, err) } } c.ServerURL, c.AllowInsecureHTTP = "http://keeporg.example.com", true if err := c.Validate(); err != nil { t.Errorf("allow_insecure_http ignored: %v", err) } }