package ssh import ( "context" "os" "path/filepath" "strings" "testing" ) func writePolicy(t *testing.T, content string) string { t.Helper() dir := t.TempDir() p := filepath.Join(dir, "ssh.json") if err := os.WriteFile(p, []byte(content), 0o600); err != nil { t.Fatalf("write policy: %v", err) } return p } func TestLoadPolicy_MissingFileNil(t *testing.T) { p, err := LoadPolicy(filepath.Join(t.TempDir(), "nope.json")) if err != nil { t.Fatalf("missing file: %v", err) } if p != nil { t.Fatalf("expected nil policy for missing file, got %+v", p) } } func TestLoadPolicy_ExpandsVars(t *testing.T) { t.Setenv("FORGE_SSH_KEY", "/keys/agent") p, err := LoadPolicy(writePolicy(t, `{ "profiles":[{"alias":"prod","host":"10.0.1.5","username":"deploy","key":"${FORGE_SSH_KEY}"}], "allowed_hosts":["10.0.*"], "default_key_path":"${FORGE_SSH_KEY}" }`)) if err != nil { t.Fatalf("load: %v", err) } pr, ok := p.Resolve("prod") if !ok { t.Fatal("profile prod not resolved") } if pr.Host != "10.0.1.5" || pr.KeyPath != "/keys/agent" { t.Fatalf("profile = %+v", pr) } if p.DefaultKeyPath != "/keys/agent" { t.Fatalf("default_key = %q", p.DefaultKeyPath) } } func TestPolicy_Allowlist(t *testing.T) { p, err := LoadPolicy(writePolicy(t, `{"allowed_hosts":["10.0.*","*.internal"]}`)) if err != nil { t.Fatalf("load: %v", err) } if !p.Allow("10.0.1.5") || !p.Allow("db.internal") { t.Fatal("expected allowed hosts to pass") } if p.Allow("192.168.0.1") || p.Allow("evil.com") { t.Fatal("expected non-allowed hosts to be denied") } } func TestPolicy_NoAllowlistAllowsAll(t *testing.T) { p, err := LoadPolicy(writePolicy(t, `{"profiles":[{"alias":"a","host":"h","username":"u"}]}`)) if err != nil { t.Fatalf("load: %v", err) } if !p.Allow("anything.example") { t.Fatal("empty allowlist should allow all (backward compat)") } } // testManager изолирует генерацию ключа в temp-каталог, чтобы тесты не // создавали ./data/id_ed25519 в модуле. func testManager(t *testing.T) *Manager { t.Helper() t.Setenv("SSH_MCP_KEY_PATH", filepath.Join(t.TempDir(), "id_ed25519")) return NewManager("") } func TestConnect_ProfileResolution_DeniesUnknown(t *testing.T) { m := testManager(t) m.SetPolicy(&Policy{ Profiles: []Profile{{Alias: "prod", Host: "10.0.1.5", Username: "deploy"}}, }) // Неизвестный профиль не должен уходить в сеть. _, err := m.Connect(context.Background(), ConnectOptions{Profile: "nope"}) if err == nil || !strings.Contains(err.Error(), "not found") { t.Fatalf("expected profile-not-found error, got %v", err) } } func TestConnect_Allowlist_DeniesHost(t *testing.T) { m := testManager(t) m.SetPolicy(&Policy{AllowedHosts: []string{"10.0.*"}}) // Host вне allowlist - deny до dial. _, err := m.Connect(context.Background(), ConnectOptions{Host: "evil.example", Username: "x"}) if err == nil || !strings.Contains(err.Error(), "not in allowed_hosts") { t.Fatalf("expected allowlist deny, got %v", err) } } func TestConnect_ProfileWithoutPolicy_Denies(t *testing.T) { m := testManager(t) _, err := m.Connect(context.Background(), ConnectOptions{Profile: "prod"}) if err == nil { t.Fatal("expected error when profile used without policy") } if !strings.Contains(err.Error(), "per-agent policy") { t.Fatalf("unexpected error: %v", err) } }