feat: complete Nexus Terminal — premium SSH/Telnet/RAW TCP desktop client
Production-minded Wails v2 desktop application with: - Full SSH client (password, key, keyboard-interactive, jump hosts, keepalive) - Telnet client (RFC 854/855 negotiation, NAWS, TTYPE, encoding) - RAW TCP client (binary-safe, text/hex/split views, payload presets) - SQLite storage with migrations and repository abstraction - Encrypted secret storage (AES-256-GCM + Argon2id) with keychain fallback - Known hosts verification (strict/ask/accept-new/insecure) - Session manager with multi-session lifecycle and Wails events - Dark charcoal Material-inspired design system (CSS custom properties) - Tabbed session management with rename, reorder, pin, context menu - Profile editor with protocol-aware form sections - Command palette, notifications, settings modal - JSON import/export, audit logging, preferences - 50+ tests across validation, secrets, storage, encoding, sessions - Makefile, build.sh, .gitignore, comprehensive README
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/copilot-ssh-client/app/internal/models"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ValidateHost
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestValidateHost(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "valid hostname", host: "example.com", wantErr: false},
|
||||
{name: "valid IP address", host: "192.168.1.1", wantErr: false},
|
||||
{name: "valid IPv6 address", host: "::1", wantErr: false},
|
||||
{name: "empty string", host: "", wantErr: true},
|
||||
{name: "only spaces", host: " ", wantErr: true},
|
||||
{name: "contains spaces", host: "host name", wantErr: true},
|
||||
{name: "contains tab", host: "host\tname", wantErr: true},
|
||||
{name: "URI scheme present", host: "ssh://example.com", wantErr: true},
|
||||
{name: "leading hyphen", host: "-bad-host", wantErr: true},
|
||||
{name: "subdomain hostname", host: "server.internal.corp", wantErr: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateHost(tt.host)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateHost(%q) error = %v, wantErr %v", tt.host, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ValidatePort
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestValidatePort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
port int
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "port 1 is valid", port: 1, wantErr: false},
|
||||
{name: "port 22 is valid", port: 22, wantErr: false},
|
||||
{name: "port 443 is valid", port: 443, wantErr: false},
|
||||
{name: "port 65535 is valid", port: 65535, wantErr: false},
|
||||
{name: "port 0 is invalid", port: 0, wantErr: true},
|
||||
{name: "negative port is invalid", port: -1, wantErr: true},
|
||||
{name: "port 65536 is invalid", port: 65536, wantErr: true},
|
||||
{name: "very large port is invalid", port: 99999, wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidatePort(tt.port)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidatePort(%d) error = %v, wantErr %v", tt.port, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ValidateUsername
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestValidateUsername(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "valid username", username: "admin", wantErr: false},
|
||||
{name: "empty string is invalid", username: "", wantErr: true},
|
||||
{name: "only whitespace is invalid", username: " ", wantErr: true},
|
||||
{name: "username with spaces is allowed", username: "my user", wantErr: false},
|
||||
{name: "very long username is valid", username: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789", wantErr: false},
|
||||
{name: "contains control character", username: "bad\x01user", wantErr: true},
|
||||
{name: "contains DEL character", username: "bad\x7fuser", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateUsername(tt.username)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateUsername(%q) error = %v, wantErr %v", tt.username, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ValidateProfile
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestValidateProfile(t *testing.T) {
|
||||
// Helper to build a valid SSH profile.
|
||||
validSSH := func() *models.Profile {
|
||||
p := models.NewProfile(models.ProtocolSSH)
|
||||
p.DisplayName = "Test SSH"
|
||||
p.Host = "example.com"
|
||||
p.Username = "root"
|
||||
return &p
|
||||
}
|
||||
|
||||
// Helper to build a valid Telnet profile.
|
||||
validTelnet := func() *models.Profile {
|
||||
p := models.NewProfile(models.ProtocolTelnet)
|
||||
p.DisplayName = "Test Telnet"
|
||||
p.Host = "switch.local"
|
||||
return &p
|
||||
}
|
||||
|
||||
// Helper to build a valid RAW profile.
|
||||
validRAW := func() *models.Profile {
|
||||
p := models.NewProfile(models.ProtocolRAW)
|
||||
p.DisplayName = "Test RAW"
|
||||
p.Host = "device.local"
|
||||
return &p
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
profile *models.Profile
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "full valid SSH profile", profile: validSSH(), wantErr: false},
|
||||
{name: "full valid Telnet profile", profile: validTelnet(), wantErr: false},
|
||||
{name: "full valid RAW profile", profile: validRAW(), wantErr: false},
|
||||
{
|
||||
name: "missing host",
|
||||
profile: func() *models.Profile {
|
||||
p := validSSH()
|
||||
p.Host = ""
|
||||
return p
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing port (zero)",
|
||||
profile: func() *models.Profile {
|
||||
p := validSSH()
|
||||
p.Port = 0
|
||||
return p
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid protocol",
|
||||
profile: func() *models.Profile {
|
||||
p := validSSH()
|
||||
p.Protocol = "ftp"
|
||||
return p
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "SSH missing username",
|
||||
profile: func() *models.Profile {
|
||||
p := validSSH()
|
||||
p.Username = ""
|
||||
return p
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "SSH nil config",
|
||||
profile: func() *models.Profile {
|
||||
p := validSSH()
|
||||
p.SSH = nil
|
||||
return p
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "negative connect timeout",
|
||||
profile: func() *models.Profile {
|
||||
p := validSSH()
|
||||
p.ConnectTimeout = -1
|
||||
return p
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "font size out of range",
|
||||
profile: func() *models.Profile {
|
||||
p := validSSH()
|
||||
p.TerminalFontSize = 0
|
||||
return p
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateProfile(tt.profile)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateProfile() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ValidateSSHConfig
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestValidateSSHConfig(t *testing.T) {
|
||||
validCfg := func() *models.SSHConfig {
|
||||
return &models.SSHConfig{
|
||||
AuthMethods: []string{"password"},
|
||||
SecretMode: models.SecretModeSaved,
|
||||
KeySecretMode: models.SecretModeAskEveryTime,
|
||||
HostKeyPolicy: models.HostKeyAsk,
|
||||
KeepaliveInterval: 30,
|
||||
KeepaliveCountMax: 3,
|
||||
JumpHosts: []models.JumpHost{},
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *models.SSHConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "valid config", cfg: validCfg(), wantErr: false},
|
||||
{
|
||||
name: "empty auth methods",
|
||||
cfg: func() *models.SSHConfig {
|
||||
c := validCfg()
|
||||
c.AuthMethods = nil
|
||||
return c
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid auth method",
|
||||
cfg: func() *models.SSHConfig {
|
||||
c := validCfg()
|
||||
c.AuthMethods = []string{"oauth"}
|
||||
return c
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid secret mode",
|
||||
cfg: func() *models.SSHConfig {
|
||||
c := validCfg()
|
||||
c.SecretMode = "wrong"
|
||||
return c
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid host key policy",
|
||||
cfg: func() *models.SSHConfig {
|
||||
c := validCfg()
|
||||
c.HostKeyPolicy = "yolo"
|
||||
return c
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateSSHConfig(tt.cfg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateSSHConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ValidateTelnetConfig
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestValidateTelnetConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *models.TelnetConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid config",
|
||||
cfg: &models.TelnetConfig{
|
||||
LineEnding: models.LineEndingCRLF,
|
||||
Encoding: "utf-8",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid line ending",
|
||||
cfg: &models.TelnetConfig{
|
||||
LineEnding: "bad",
|
||||
Encoding: "utf-8",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty encoding",
|
||||
cfg: &models.TelnetConfig{
|
||||
LineEnding: models.LineEndingLF,
|
||||
Encoding: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateTelnetConfig(tt.cfg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateTelnetConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ValidateRAWConfig
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestValidateRAWConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *models.RAWConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid config",
|
||||
cfg: &models.RAWConfig{
|
||||
LineEnding: models.LineEndingCRLF,
|
||||
Encoding: "utf-8",
|
||||
PayloadPresets: []models.PayloadPreset{},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid line ending",
|
||||
cfg: &models.RAWConfig{
|
||||
LineEnding: "bad",
|
||||
Encoding: "utf-8",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty encoding",
|
||||
cfg: &models.RAWConfig{
|
||||
LineEnding: models.LineEndingLF,
|
||||
Encoding: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "preset with invalid format",
|
||||
cfg: &models.RAWConfig{
|
||||
LineEnding: models.LineEndingCRLF,
|
||||
Encoding: "utf-8",
|
||||
PayloadPresets: []models.PayloadPreset{
|
||||
{Name: "ping", Format: "binary"},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "preset with empty name",
|
||||
cfg: &models.RAWConfig{
|
||||
LineEnding: models.LineEndingCRLF,
|
||||
Encoding: "utf-8",
|
||||
PayloadPresets: []models.PayloadPreset{
|
||||
{Name: "", Format: "text"},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateRAWConfig(tt.cfg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateRAWConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user