using Tests.EndToEnd.Infra; using Xunit.Abstractions; namespace Tests.EndToEnd.ExporterTests; [Collection("Yaml CLI tests")] public class SshExportWorkflowTests( TempYamlCliFixture fs, ITestOutputHelper outputHelper) : IClassFixture { private async Task<(string output, string yaml)> ExecuteAsync(params string[] args) { outputHelper.WriteLine($"rpk {string.Join(" ", args)}"); var output = await YamlCliTestHost.RunAsync( args, fs.Root, outputHelper, "config.yaml"); outputHelper.WriteLine(output); var yaml = await File.ReadAllTextAsync(Path.Combine(fs.Root, "config.yaml")); return (output, yaml); } [Fact] public async Task ssh_export_basic_workflow_test() { await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), """ version: 1 resources: - kind: System type: vm name: vm-web01 tags: - prod labels: ip: 192.168.1.10 ssh_user: ubuntu - kind: System type: vm name: vm-db01 labels: ip: 192.168.1.20 ssh_user: postgres """); (var output, var _) = await ExecuteAsync( "ssh", "export" ); Assert.Equal(""" Generated SSH Config: Host vm-db01 HostName 192.168.1.20 User postgres Host vm-web01 HostName 192.168.1.10 User ubuntu """, output); } [Fact] public async Task ssh_export_with_defaults_test() { await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), """ version: 1 resources: - kind: System type: vm name: vm1 labels: ip: 10.0.0.1 """); (var output, var _) = await ExecuteAsync( "ssh", "export", "--default-user", "admin", "--default-port", "2222", "--default-identity", "~/.ssh/id_rsa" ); Assert.Equal(""" Generated SSH Config: Host vm1 HostName 10.0.0.1 User admin Port 2222 IdentityFile ~/.ssh/id_rsa """, output); } [Fact] public async Task ssh_export_respects_tag_filter() { await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), """ version: 1 resources: - kind: System type: vm name: prod-vm tags: - prod labels: ip: 10.0.0.1 - kind: System type: vm name: staging-vm tags: - staging labels: ip: 10.0.0.2 """); (var output, var _) = await ExecuteAsync( "ssh", "export", "--include-tags", "prod" ); Assert.Contains("Host prod-vm", output); Assert.DoesNotContain("Host staging-vm", output); } [Fact] public async Task ssh_export_is_sorted_by_name() { await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), """ version: 1 resources: - kind: System type: vm name: b-host labels: ip: 10.0.0.2 - kind: System type: vm name: a-host labels: ip: 10.0.0.1 """); (var output, var _) = await ExecuteAsync( "ssh", "export" ); var aIndex = output.IndexOf("Host a-host", StringComparison.Ordinal); var bIndex = output.IndexOf("Host b-host", StringComparison.Ordinal); Assert.True(aIndex < bIndex); } [Fact] public async Task ssh_export_uses_hostname_label_when_no_ip() { // ssh-config-export.md §1 & §9: `hostname` is an accepted address // fallback when `ip` is not set. await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), """ version: 1 resources: - kind: System type: vm name: vm-host labels: hostname: vm-host.lan """); (var output, var _) = await ExecuteAsync("ssh", "export"); Assert.Contains("Host vm-host", output); Assert.Contains("HostName vm-host.lan", output); } [Fact] public async Task ssh_export_uses_ansible_host_label_when_no_ip_or_hostname() { // ssh-config-export.md §1 & §9: `ansible_host` is the third fallback // for the address. await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), """ version: 1 resources: - kind: System type: vm name: vm-ansible labels: ansible_host: 10.0.0.99 """); (var output, var _) = await ExecuteAsync("ssh", "export"); Assert.Contains("Host vm-ansible", output); Assert.Contains("HostName 10.0.0.99", output); } [Fact] public async Task ssh_export_falls_back_from_ssh_user_to_ansible_user() { // ssh-config-export.md §9 fallback chain: ssh_user → ansible_user → // CLI default. A resource with only `ansible_user` should still // produce a User line. await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), """ version: 1 resources: - kind: System type: vm name: vm-prefer labels: ip: 10.0.0.1 ssh_user: prefer-this ansible_user: ignore-this - kind: System type: vm name: vm-fallback labels: ip: 10.0.0.2 ansible_user: from-ansible """); (var output, var _) = await ExecuteAsync("ssh", "export"); Assert.Contains(""" Host vm-fallback HostName 10.0.0.2 User from-ansible """, output); Assert.Contains(""" Host vm-prefer HostName 10.0.0.1 User prefer-this """, output); Assert.DoesNotContain("ignore-this", output); } [Fact] public async Task ssh_export_falls_back_from_ssh_port_to_ansible_port() { // ssh-config-export.md §9 fallback chain: ssh_port → ansible_port → // CLI default. Note: the generator omits `Port` if the resolved port // equals 22, so test with a non-default value. await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), """ version: 1 resources: - kind: System type: vm name: vm-prefer labels: ip: 10.0.0.1 ssh_port: "2200" ansible_port: "9999" - kind: System type: vm name: vm-fallback labels: ip: 10.0.0.2 ansible_port: "2222" """); (var output, var _) = await ExecuteAsync("ssh", "export"); Assert.Contains(""" Host vm-fallback HostName 10.0.0.2 Port 2222 """, output); Assert.Contains(""" Host vm-prefer HostName 10.0.0.1 Port 2200 """, output); Assert.DoesNotContain("9999", output); } [Fact] public async Task ssh_export_honours_per_resource_ssh_port_and_identity_file_labels() { // ssh-config-export.md §2: per-resource ssh_port and // ssh_identity_file labels are emitted as Port / IdentityFile lines // even when no CLI defaults are passed. await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), """ version: 1 resources: - kind: System type: vm name: vm-custom labels: ip: 10.0.0.1 ssh_user: ubuntu ssh_port: "2222" ssh_identity_file: ~/.ssh/id_custom """); (var output, var _) = await ExecuteAsync("ssh", "export"); Assert.Contains(""" Host vm-custom HostName 10.0.0.1 User ubuntu Port 2222 IdentityFile ~/.ssh/id_custom """, output); } [Fact] public async Task ssh_export_skips_resources_without_address() { await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), """ version: 1 resources: - kind: System type: vm name: vm-with-ip labels: ip: 10.0.0.1 - kind: System type: vm name: vm-no-ip """); (var output, var _) = await ExecuteAsync( "ssh", "export" ); Assert.Contains("Host vm-with-ip", output); Assert.DoesNotContain("vm-no-ip", output); } }