package infra import ( "fmt" "net" "testing" "time" ) func TestSSHTunnelOpenClose(t *testing.T) { conn := skipIfNoSSH(t) t.Run("abre tunel y lo cierra", func(t *testing.T) { // Puerto efimero libre: un puerto fijo daba "address already in use" // cuando el paquete corre con -count o concurrentemente con otra // ejecucion de `go test` del mismo paquete. localPort := freeTCPPort(t) // Tunel a localhost:22 del remoto (el propio sshd) pid, err := SSHTunnelOpen(conn, localPort, "localhost", 22) if err != nil { t.Fatalf("SSHTunnelOpen: %v", err) } if pid <= 0 { t.Fatalf("PID invalido: %d", pid) } // Verificar que el puerto local esta escuchando time.Sleep(300 * time.Millisecond) c, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", localPort), 2*time.Second) if err != nil { // Limpiar antes de fallar SSHTunnelClose(pid) t.Fatalf("no se puede conectar al tunel en localhost:%d: %v", localPort, err) } c.Close() // Cerrar tunel err = SSHTunnelClose(pid) if err != nil { t.Errorf("SSHTunnelClose: %v", err) } // Verificar que el puerto ya no escucha time.Sleep(300 * time.Millisecond) c, err = net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", localPort), 1*time.Second) if err == nil { c.Close() t.Error("el tunel sigue abierto despues de SSHTunnelClose") } }) } // freeTCPPort asks the kernel for a free TCP port on loopback by binding to // port 0, reading the assigned port, and releasing it. A small race window // exists before the caller reuses the port, but it avoids the hard collisions // of a fixed port across concurrent or repeated test runs. func freeTCPPort(t *testing.T) int { t.Helper() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("freeTCPPort: %v", err) } defer l.Close() return l.Addr().(*net.TCPAddr).Port }