Files
lazyssh/internal/adapters/ui/validation_test.go
T
2025-09-19 10:49:29 +01:00

330 lines
9.1 KiB
Go

// Copyright 2025.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ui
import (
"os"
"path/filepath"
"testing"
)
func TestValidateHost(t *testing.T) {
tests := []struct {
name string
host string
wantErr bool
}{
{"Valid IP", "192.168.1.1", false},
{"Valid hostname", "example.com", false},
{"Valid subdomain", "api.example.com", false},
{"Empty host", "", true},
{"Host with spaces", "example .com", true},
{"Host with invalid chars", "example@com", true},
{"Host starting with dot", ".example.com", true},
{"Host ending with dot", "example.com.", true},
{"Host with empty label", "example..com", true},
{"Label starting with hyphen", "-example.com", true},
{"Label ending with hyphen", "example-.com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateHost(tt.host)
if (err != nil) != tt.wantErr {
t.Errorf("validateHost(%s) error = %v, wantErr %v", tt.host, err, tt.wantErr)
}
})
}
}
func TestValidatePortForward(t *testing.T) {
tests := []struct {
name string
forward string
wantErr bool
}{
{"Valid simple forward", "8080:localhost:80", false},
{"Valid with bind address", "127.0.0.1:8080:localhost:80", false},
{"Multiple forwards", "8080:localhost:80, 3000:localhost:3000", false},
{"Empty forward", "", false},
{"Invalid format - too few parts", "8080:localhost", true},
{"Invalid format - too many parts", "127.0.0.1:8080:localhost:80:extra", true},
{"Invalid port number", "abc:localhost:80", true},
{"Port out of range", "70000:localhost:80", true},
{"Invalid bind address - malformed IP", "127.0.0.0.0.0.1:8080:localhost:80", true},
{"Invalid bind address - IP out of range", "192.168.1.256:8080:localhost:80", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validatePortForward(tt.forward)
if (err != nil) != tt.wantErr {
t.Errorf("validatePortForward(%s) error = %v, wantErr %v", tt.forward, err, tt.wantErr)
}
})
}
}
func TestValidateDynamicForward(t *testing.T) {
tests := []struct {
name string
forward string
wantErr bool
}{
{"Valid port only", "1080", false},
{"Valid with bind address", "127.0.0.1:1080", false},
{"Multiple forwards", "1080, 1081", false},
{"Empty forward", "", false},
{"Invalid format - too many parts", "127.0.0.1:1080:extra", true},
{"Invalid port number", "abc", true},
{"Port out of range", "70000", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateDynamicForward(tt.forward)
if (err != nil) != tt.wantErr {
t.Errorf("validateDynamicForward(%s) error = %v, wantErr %v", tt.forward, err, tt.wantErr)
}
})
}
}
func TestValidateBindAddress(t *testing.T) {
tests := []struct {
name string
address string
wantErr bool
}{
{"Valid IP", "192.168.1.1", false},
{"Valid IPv6", "::1", false},
{"Valid hostname", "example.com", false},
{"Wildcard", "*", false},
{"Localhost", "localhost", false},
{"Empty address", "", false},
{"Address with spaces", "example .com", true},
{"Address with invalid chars", "example@com", true},
{"Address starting with dot", ".example.com", true},
{"Address ending with dot", "example.com.", true},
{"Address starting with hyphen", "-example.com", true},
{"Address ending with hyphen", "example-.com", true},
{"Invalid IP-like address", "127.0.0.0.0.0.1", true},
{"Invalid numeric hostname", "192.168.1.256", true},
{"Multiple dots", "example..com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateBindAddress(tt.address)
if (err != nil) != tt.wantErr {
t.Errorf("validateBindAddress(%s) error = %v, wantErr %v", tt.address, err, tt.wantErr)
}
})
}
}
func TestValidateKeyPaths(t *testing.T) {
// Prepare an isolated HOME with a mock .ssh folder and key files
oldHome := os.Getenv("HOME")
t.Cleanup(func() {
_ = os.Setenv("HOME", oldHome)
})
tempHome := t.TempDir()
sshDir := filepath.Join(tempHome, ".ssh")
if err := os.MkdirAll(sshDir, 0o755); err != nil {
t.Fatalf("failed to create temp .ssh dir: %v", err)
}
shouldExistFiles := []string{"id_rsa", "id_ed25519"}
for _, name := range shouldExistFiles {
p := filepath.Join(sshDir, name)
if err := os.WriteFile(p, []byte("test"), 0o644); err != nil {
t.Fatalf("failed to create mock key file %s: %v", p, err)
}
}
if err := os.Setenv("HOME", tempHome); err != nil {
t.Fatalf("failed to set HOME: %v", err)
}
tests := []struct {
name string
keys string
wantErr bool
}{
{"Valid single path", "~/.ssh/id_rsa", false},
{"Valid multiple paths", "~/.ssh/id_rsa, ~/.ssh/id_ed25519", false},
{"Empty keys", "", false},
{"Path with newline", "~/.ssh/id_rsa\n", true},
{"Path with tab", "~/.ssh/id_rsa\t", true},
{"Path with carriage return", "~/.ssh/id_rsa\r", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateKeyPaths(tt.keys)
if (err != nil) != tt.wantErr {
t.Errorf("validateKeyPaths(%s) error = %v, wantErr %v", tt.keys, err, tt.wantErr)
}
})
}
}
func TestFieldValidatorPatterns(t *testing.T) {
fieldValidators := GetFieldValidators()
tests := []struct {
field string
value string
wantErr bool
}{
// Alias field
{"Alias", "server-01", false},
{"Alias", "server_01", false},
{"Alias", "server.01", false},
{"Alias", "server@01", true},
{"Alias", "", true}, // Required field
// Port field
{"Port", "22", false},
{"Port", "65535", false},
{"Port", "0", true},
{"Port", "65536", true},
{"Port", "abc", true},
// User field
{"User", "root", false},
{"User", "user_name", false},
{"User", "user-name", false},
{"User", "1user", true}, // Can't start with number
// ConnectTimeout field
{"ConnectTimeout", "none", false},
{"ConnectTimeout", "30", false},
{"ConnectTimeout", "0", true},
{"ConnectTimeout", "-10", true},
// IPQoS field
{"IPQoS", "af21 cs1", false},
{"IPQoS", "ef", false},
{"IPQoS", "lowdelay", false},
{"IPQoS", "invalid", true},
// EscapeChar field
{"EscapeChar", "~", false},
{"EscapeChar", "none", false},
{"EscapeChar", "^A", false},
{"EscapeChar", "^z", false},
{"EscapeChar", "invalid", true},
}
for _, tt := range tests {
t.Run(tt.field+"_"+tt.value, func(t *testing.T) {
validator, exists := fieldValidators[tt.field]
if !exists {
if tt.wantErr {
t.Errorf("Expected validator for field %s but none found", tt.field)
}
return
}
var err error
// Check required fields
switch {
case validator.Required && tt.value == "":
err = &testError{msg: "required field is empty"}
case validator.Pattern != nil && !validator.Pattern.MatchString(tt.value):
err = &testError{msg: "pattern mismatch"}
case validator.Validate != nil:
err = validator.Validate(tt.value)
}
if (err != nil) != tt.wantErr {
t.Errorf("validateField(%s, %s) error = %v, wantErr %v", tt.field, tt.value, err, tt.wantErr)
}
})
}
}
// testError is a helper type for testing
type testError struct {
msg string
}
func (e *testError) Error() string {
return e.msg
}
func TestValidationState_MultipleErrors(t *testing.T) {
state := NewValidationState()
// Set multiple errors
state.SetError("Alias", "Alias is required")
state.SetError("Host", "Host is required")
state.SetError("Port", "Port must be between 1 and 65535")
state.SetError("User", "Invalid username")
// Check that we have errors
if !state.HasErrors() {
t.Error("Expected HasErrors to return true")
}
// Get all errors
errors := state.GetAllErrors()
// Should have 4 errors
if len(errors) != 4 {
t.Errorf("Expected 4 errors, got %d", len(errors))
}
// Print errors for debugging
t.Logf("Found %d errors:", len(errors))
for i, err := range errors {
t.Logf(" %d. %s", i+1, err)
}
// Check that errors are in the expected order
expectedOrder := []string{"Alias", "Host", "Port", "User"}
for i, expectedField := range expectedOrder {
if i >= len(errors) {
break
}
// Check if the error message starts with the expected field name
if len(errors[i]) < len(expectedField) || errors[i][:len(expectedField)] != expectedField {
t.Errorf("Expected error %d to be for field %s, but got: %s", i, expectedField, errors[i])
}
}
}
func TestValidationState_Clear(t *testing.T) {
state := NewValidationState()
// Add some errors
state.SetError("Alias", "Error 1")
state.SetError("Host", "Error 2")
// Clear all errors
state.Clear()
// Should have no errors
if state.HasErrors() {
t.Error("Expected no errors after Clear()")
}
if state.GetErrorCount() != 0 {
t.Errorf("Expected error count to be 0, got %d", state.GetErrorCount())
}
}