package keymgr import ( "strings" "testing" "ssh-access-tui/internal/model" ) func TestHostAliases(t *testing.T) { tests := []struct { name string forwards []model.Forward want []string }{ { name: "empty", forwards: nil, want: []string{}, }, { name: "single service per host uses bare host name", forwards: []model.Forward{ {HostName: "db-prod-1", ServiceName: "ssh"}, {HostName: "web-1", ServiceName: "https"}, }, want: []string{"db-prod-1", "web-1"}, }, { name: "multiple services on same host get host-service alias", forwards: []model.Forward{ {HostName: "db-prod-1", ServiceName: "ssh"}, {HostName: "db-prod-1", ServiceName: "web-admin"}, }, want: []string{"db-prod-1-ssh", "db-prod-1-web-admin"}, }, { name: "mixed: one host with a single service, another with several", forwards: []model.Forward{ {HostName: "db-prod-1", ServiceName: "ssh"}, {HostName: "db-prod-1", ServiceName: "web-admin"}, {HostName: "cache-1", ServiceName: "redis"}, }, want: []string{"db-prod-1-ssh", "db-prod-1-web-admin", "cache-1"}, }, { name: "order is preserved, not sorted", forwards: []model.Forward{ {HostName: "zzz-host", ServiceName: "ssh"}, {HostName: "aaa-host", ServiceName: "ssh"}, }, want: []string{"zzz-host", "aaa-host"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := hostAliases(tt.forwards) if len(got) != len(tt.want) { t.Fatalf("hostAliases() = %v, want %v", got, tt.want) } for i := range got { if got[i] != tt.want[i] { t.Errorf("hostAliases()[%d] = %q, want %q (full: got=%v want=%v)", i, got[i], tt.want[i], got, tt.want) } } }) } } func TestRenderClientConfigProxyJump(t *testing.T) { forwards := []model.Forward{ {HostName: "db-prod-1", Address: "10.0.0.5", Port: 22, ServiceName: "ssh", Description: "primary db"}, {HostName: "db-prod-1", Address: "10.0.0.5", Port: 8080, ServiceName: "web-admin"}, {HostName: "cache-1", Address: "10.0.0.9", Port: 6379, ServiceName: "redis"}, } out := RenderClientConfigProxyJump("bastion.example.com", 2222, "a-makarov", "a-makarov_jump_key", forwards) // The one always-present "jump" block. for _, want := range []string{ "Host jump\n", " HostName bastion.example.com\n", " Port 2222\n", " User a-makarov\n", " IdentityFile ~/.ssh/a-makarov_jump_key\n", } { if !strings.Contains(out, want) { t.Errorf("output missing jump block line %q\nfull output:\n%s", want, out) } } // LocalForward is the LocalForward-mode marker; it must never appear here. if strings.Contains(out, "LocalForward") { t.Errorf("ProxyJump-mode output must not contain LocalForward, got:\n%s", out) } // Every forward gets its own Host block, with the expected alias // (deduplicated per hostAliases), ProxyJump jump, and no local port. wantBlocks := []struct { alias string addr string port string }{ {"db-prod-1-ssh", "10.0.0.5", "22"}, {"db-prod-1-web-admin", "10.0.0.5", "8080"}, {"cache-1", "10.0.0.9", "6379"}, } for _, wb := range wantBlocks { if !strings.Contains(out, "Host "+wb.alias+"\n") { t.Errorf("output missing %q Host block, got:\n%s", wb.alias, out) } } if !strings.Contains(out, " ProxyJump jump\n") { t.Errorf("output missing ProxyJump jump directive, got:\n%s", out) } proxyJumpCount := strings.Count(out, "ProxyJump jump") if proxyJumpCount != len(forwards) { t.Errorf("expected %d ProxyJump jump directives (one per forward), got %d", len(forwards), proxyJumpCount) } identitiesOnlyCount := strings.Count(out, "IdentitiesOnly yes") // One for the "jump" block plus one per forward block. if identitiesOnlyCount != len(forwards)+1 { t.Errorf("expected %d IdentitiesOnly yes lines, got %d", len(forwards)+1, identitiesOnlyCount) } } func TestRenderClientConfigProxyJump_NoForwards(t *testing.T) { out := RenderClientConfigProxyJump("bastion.example.com", 22, "nobody", "nobody_jump_key", nil) if !strings.Contains(out, "Host jump\n") { t.Errorf("expected jump block even with no forwards, got:\n%s", out) } if strings.Contains(out, "ProxyJump jump") { t.Errorf("no forwards means no per-service blocks, but found ProxyJump jump in:\n%s", out) } } // TestRenderClientConfig_LocalForwardModeUnaffected pins down that the // pre-existing LocalForward-mode renderer still behaves as before adding // ProxyJump mode alongside it (regression guard). func TestRenderClientConfig_LocalForwardModeUnaffected(t *testing.T) { forwards := []AllocatedForward{ {Forward: model.Forward{HostName: "db-prod-1", Address: "10.0.0.5", Port: 22, ServiceName: "ssh", Description: "primary db"}, LocalPort: 20000}, {Forward: model.Forward{HostName: "cache-1", Address: "10.0.0.9", Port: 6379, ServiceName: "redis"}, LocalPort: 20001}, } out := RenderClientConfig("bastion.example.com", 2222, "a-makarov", "a-makarov_jump_key", forwards) if strings.Contains(out, "ProxyJump") { t.Errorf("LocalForward-mode output must not contain ProxyJump, got:\n%s", out) } if !strings.Contains(out, "Host jump\n") { t.Errorf("expected single jump block, got:\n%s", out) } if strings.Contains(out, "Host db-prod-1\n") || strings.Contains(out, "Host cache-1\n") { t.Errorf("LocalForward mode must not emit per-service Host blocks, got:\n%s", out) } wantLines := []string{ " LocalForward 20000 10.0.0.5:22 # db-prod-1/ssh: primary db\n", " LocalForward 20001 10.0.0.9:6379 # cache-1/redis\n", } for _, want := range wantLines { if !strings.Contains(out, want) { t.Errorf("missing expected LocalForward line %q, got:\n%s", want, out) } } }