ab7317b0c0
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.
280 lines
7.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|