Files
fn_registry/functions/infra/ssh_config_parse_test.go
egutierrez ab7317b0c0 feat: add Go SSH config management functions and type
7 funciones Go del dominio infra para gestion programatica de ~/.ssh/config:
ssh_config_parse (parser de bloques Host/Match), ssh_config_read (lectura del
archivo), ssh_config_find (busqueda por host), ssh_config_add_entry y
ssh_config_remove_entry (CRUD), ssh_config_render (serializacion a texto),
ssh_config_write (escritura atomica). Incluye tipo SshConfigEntry (product type)
y tests unitarios del parser.
2026-04-12 13:54:43 +02:00

280 lines
7.1 KiB
Go

package infra
import (
"strings"
"testing"
)
func TestSSHConfigParse_MultipleHosts(t *testing.T) {
content := `
Host prod
HostName 10.0.0.1
User admin
Port 22
IdentityFile ~/.ssh/id_prod
Host staging
HostName 10.0.0.2
User deploy
Port 2222
`
entries := SSHConfigParse(content)
if len(entries) != 2 {
t.Fatalf("expected 2 entries, got %d", len(entries))
}
if entries[0].Alias != "prod" {
t.Errorf("expected alias prod, got %s", entries[0].Alias)
}
if entries[0].HostName != "10.0.0.1" {
t.Errorf("expected hostname 10.0.0.1, got %s", entries[0].HostName)
}
if entries[0].User != "admin" {
t.Errorf("expected user admin, got %s", entries[0].User)
}
if entries[0].Port != 22 {
t.Errorf("expected port 22, got %d", entries[0].Port)
}
if entries[0].IdentityFile != "~/.ssh/id_prod" {
t.Errorf("expected identity file ~/.ssh/id_prod, got %s", entries[0].IdentityFile)
}
if entries[1].Alias != "staging" {
t.Errorf("expected alias staging, got %s", entries[1].Alias)
}
if entries[1].Port != 2222 {
t.Errorf("expected port 2222, got %d", entries[1].Port)
}
}
func TestSSHConfigParse_IgnoresCommentsAndBlanks(t *testing.T) {
content := `
# This is a comment
Host myserver
HostName 192.168.1.1
# inline comment
User root
`
entries := SSHConfigParse(content)
if len(entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(entries))
}
if entries[0].User != "root" {
t.Errorf("expected user root, got %s", entries[0].User)
}
}
func TestSSHConfigParse_IgnoresWildcards(t *testing.T) {
content := `
Host *
ServerAliveInterval 60
Host prod
HostName 10.0.0.1
Host 192.168.*
User local
`
entries := SSHConfigParse(content)
if len(entries) != 1 {
t.Fatalf("expected 1 entry (wildcards ignored), got %d", len(entries))
}
if entries[0].Alias != "prod" {
t.Errorf("expected alias prod, got %s", entries[0].Alias)
}
}
func TestSSHConfigParse_ExtraOptions(t *testing.T) {
content := `Host jump
HostName bastion.example.com
User ops
ForwardAgent yes
ProxyJump none
`
entries := SSHConfigParse(content)
if len(entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(entries))
}
if entries[0].Options["ForwardAgent"] != "yes" {
t.Errorf("expected ForwardAgent=yes, got %s", entries[0].Options["ForwardAgent"])
}
if entries[0].Options["ProxyJump"] != "none" {
t.Errorf("expected ProxyJump=none, got %s", entries[0].Options["ProxyJump"])
}
}
func TestSSHConfigRender_FullEntry(t *testing.T) {
entries := []SSHConfigEntry{{
Alias: "prod",
HostName: "10.0.0.1",
User: "admin",
Port: 2222,
IdentityFile: "~/.ssh/id_prod",
}}
result := SSHConfigRender(entries)
if !strings.Contains(result, "Host prod") {
t.Error("missing Host directive")
}
if !strings.Contains(result, "HostName 10.0.0.1") {
t.Error("missing HostName")
}
if !strings.Contains(result, "User admin") {
t.Error("missing User")
}
if !strings.Contains(result, "Port 2222") {
t.Error("missing Port")
}
if !strings.Contains(result, "IdentityFile ~/.ssh/id_prod") {
t.Error("missing IdentityFile")
}
}
func TestSSHConfigRender_MinimalEntry(t *testing.T) {
entries := []SSHConfigEntry{{Alias: "local"}}
result := SSHConfigRender(entries)
if result != "Host local\n" {
t.Errorf("expected minimal render, got %q", result)
}
}
func TestSSHConfigRender_MultipleEntries(t *testing.T) {
entries := []SSHConfigEntry{
{Alias: "a", HostName: "1.1.1.1"},
{Alias: "b", HostName: "2.2.2.2"},
}
result := SSHConfigRender(entries)
parts := strings.Split(result, "\n\n")
if len(parts) < 2 {
t.Error("expected blocks separated by blank line")
}
}
func TestSSHConfigRender_OptionsSorted(t *testing.T) {
entries := []SSHConfigEntry{{
Alias: "test",
HostName: "1.1.1.1",
Options: map[string]string{"ProxyJump": "bastion", "ForwardAgent": "yes"},
}}
result := SSHConfigRender(entries)
fwIdx := strings.Index(result, "ForwardAgent")
pjIdx := strings.Index(result, "ProxyJump")
if fwIdx < 0 || pjIdx < 0 {
t.Fatal("missing options in render")
}
if fwIdx > pjIdx {
t.Error("options should be sorted alphabetically")
}
}
func TestSSHConfigFind_Exists(t *testing.T) {
entries := []SSHConfigEntry{
{Alias: "prod", HostName: "10.0.0.1"},
{Alias: "staging", HostName: "10.0.0.2"},
}
entry, ok := SSHConfigFind(entries, "staging")
if !ok {
t.Fatal("expected to find staging")
}
if entry.HostName != "10.0.0.2" {
t.Errorf("expected hostname 10.0.0.2, got %s", entry.HostName)
}
}
func TestSSHConfigFind_NotExists(t *testing.T) {
entries := []SSHConfigEntry{{Alias: "prod"}}
_, ok := SSHConfigFind(entries, "nope")
if ok {
t.Error("expected not found")
}
}
func TestSSHConfigAddEntry_ToEmpty(t *testing.T) {
entry := SSHConfigEntry{Alias: "new", HostName: "1.1.1.1"}
result, err := SSHConfigAddEntry(nil, entry)
if err != nil {
t.Fatal(err)
}
if len(result) != 1 || result[0].Alias != "new" {
t.Errorf("unexpected result: %+v", result)
}
}
func TestSSHConfigAddEntry_ToExisting(t *testing.T) {
existing := []SSHConfigEntry{{Alias: "old"}}
entry := SSHConfigEntry{Alias: "new"}
result, err := SSHConfigAddEntry(existing, entry)
if err != nil {
t.Fatal(err)
}
if len(result) != 2 {
t.Fatalf("expected 2 entries, got %d", len(result))
}
// Original no mutado
if len(existing) != 1 {
t.Error("original slice was mutated")
}
}
func TestSSHConfigAddEntry_DuplicateAlias(t *testing.T) {
existing := []SSHConfigEntry{{Alias: "prod"}}
entry := SSHConfigEntry{Alias: "prod"}
_, err := SSHConfigAddEntry(existing, entry)
if err == nil {
t.Error("expected error for duplicate alias")
}
}
func TestSSHConfigRemoveEntry_Exists(t *testing.T) {
entries := []SSHConfigEntry{
{Alias: "a"},
{Alias: "b"},
{Alias: "c"},
}
result, err := SSHConfigRemoveEntry(entries, "b")
if err != nil {
t.Fatal(err)
}
if len(result) != 2 {
t.Fatalf("expected 2 entries, got %d", len(result))
}
if result[0].Alias != "a" || result[1].Alias != "c" {
t.Errorf("unexpected order: %+v", result)
}
// Original no mutado
if len(entries) != 3 {
t.Error("original slice was mutated")
}
}
func TestSSHConfigRemoveEntry_NotExists(t *testing.T) {
entries := []SSHConfigEntry{{Alias: "prod"}}
_, err := SSHConfigRemoveEntry(entries, "nope")
if err == nil {
t.Error("expected error for missing alias")
}
}
func TestSSHConfigParseRender_Roundtrip(t *testing.T) {
original := []SSHConfigEntry{
{Alias: "prod", HostName: "10.0.0.1", User: "admin", Port: 22, IdentityFile: "~/.ssh/id_prod"},
{Alias: "staging", HostName: "10.0.0.2", User: "deploy", Port: 2222},
}
text := SSHConfigRender(original)
parsed := SSHConfigParse(text)
if len(parsed) != len(original) {
t.Fatalf("roundtrip: expected %d entries, got %d", len(original), len(parsed))
}
for i := range original {
if parsed[i].Alias != original[i].Alias {
t.Errorf("roundtrip[%d]: alias mismatch %s != %s", i, parsed[i].Alias, original[i].Alias)
}
if parsed[i].HostName != original[i].HostName {
t.Errorf("roundtrip[%d]: hostname mismatch", i)
}
if parsed[i].User != original[i].User {
t.Errorf("roundtrip[%d]: user mismatch", i)
}
if parsed[i].Port != original[i].Port {
t.Errorf("roundtrip[%d]: port mismatch", i)
}
}
}