Files
go-i2ptunnel-config/lib/comprehensive_test.go

694 lines
18 KiB
Go

package i2pconv
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
// TestCrossFormatConversion tests comprehensive conversion between all supported formats
// This covers the primary conversion paths: properties ↔ yaml ↔ ini
func TestCrossFormatConversion(t *testing.T) {
// Base tunnel configuration that works across all formats
baseConfig := &TunnelConfig{
Name: "TestTunnel",
Type: "httpclient",
Interface: "127.0.0.1",
Port: 8080,
Target: "example.i2p",
Description: "Cross-format test tunnel",
PersistentKey: true,
I2CP: map[string]interface{}{
"leaseSetEncType": []string{"4", "0"},
"reduceIdleTime": 900000,
},
Tunnel: map[string]interface{}{
"proxyList": "proxy1.i2p,proxy2.i2p",
"sharedClient": true,
"useCompression": true,
},
Inbound: map[string]interface{}{
"length": 3,
},
Outbound: map[string]interface{}{
"length": 2,
},
}
converter := &Converter{strict: false}
formats := []string{"properties", "yaml", "ini"}
// Test round-trip conversions for each format
for _, format := range formats {
t.Run("round-trip_"+format, func(t *testing.T) {
// Convert config to format
output1, err := converter.generateOutput(baseConfig, format)
if err != nil {
t.Fatalf("Failed to generate %s: %v", format, err)
}
// Parse it back
config1, err := converter.ParseInput(output1, format)
if err != nil {
t.Fatalf("Failed to parse generated %s: %v", format, err)
}
// Convert back to same format
output2, err := converter.generateOutput(config1, format)
if err != nil {
t.Fatalf("Failed to re-generate %s: %v", format, err)
}
// Parse again to ensure consistency
config2, err := converter.ParseInput(output2, format)
if err != nil {
t.Fatalf("Failed to re-parse %s: %v", format, err)
}
// Verify basic fields are preserved
if config1.Name != config2.Name {
t.Errorf("Round-trip %s Name: expected %q, got %q", format, config1.Name, config2.Name)
}
if config1.Type != config2.Type {
t.Errorf("Round-trip %s Type: expected %q, got %q", format, config1.Type, config2.Type)
}
if config1.Interface != config2.Interface {
t.Errorf("Round-trip %s Interface: expected %q, got %q", format, config1.Interface, config2.Interface)
}
if config1.Port != config2.Port {
t.Errorf("Round-trip %s Port: expected %d, got %d", format, config1.Port, config2.Port)
}
t.Logf("%s round-trip successful", strings.ToUpper(format))
})
}
// Test cross-format conversion paths (properties → yaml → ini → properties)
t.Run("cross-format_conversion_chain", func(t *testing.T) {
// Start with properties
propsOutput, err := converter.generateOutput(baseConfig, "properties")
if err != nil {
t.Fatalf("Failed to generate properties: %v", err)
}
// Properties → YAML
propsConfig, err := converter.ParseInput(propsOutput, "properties")
if err != nil {
t.Fatalf("Failed to parse properties: %v", err)
}
// The YAML parseInput doesn't handle the wrapper format that generateYAML produces -
// we need to test direct conversion instead
_, err = converter.Convert(propsOutput, "properties", "yaml")
if err != nil {
t.Fatalf("Failed direct properties to YAML conversion: %v", err)
}
// Test INI conversion
iniOutput, err := converter.generateOutput(propsConfig, "ini")
if err != nil {
t.Fatalf("Failed to convert properties to INI: %v", err)
}
// INI → Properties (complete a partial circle)
iniConfig, err := converter.ParseInput(iniOutput, "ini")
if err != nil {
t.Fatalf("Failed to parse INI: %v", err)
}
finalPropsOutput, err := converter.generateOutput(iniConfig, "properties")
if err != nil {
t.Fatalf("Failed to convert INI back to properties: %v", err)
}
// Parse final result and verify core fields are preserved
finalConfig, err := converter.ParseInput(finalPropsOutput, "properties")
if err != nil {
t.Fatalf("Failed to parse final properties: %v", err)
}
// Verify essential data is preserved through the conversion chain
if finalConfig.Name != propsConfig.Name {
t.Errorf("Cross-format Name: expected %q, got %q", propsConfig.Name, finalConfig.Name)
}
if finalConfig.Type != propsConfig.Type {
t.Errorf("Cross-format Type: expected %q, got %q", propsConfig.Type, finalConfig.Type)
}
if finalConfig.Interface != propsConfig.Interface {
t.Errorf("Cross-format Interface: expected %q, got %q", propsConfig.Interface, finalConfig.Interface)
}
if finalConfig.Port != propsConfig.Port {
t.Errorf("Cross-format Port: expected %d, got %d", propsConfig.Port, finalConfig.Port)
}
t.Log("Cross-format conversion chain completed successfully")
})
}
// TestYAMLParser tests the parseYAML function with the nested tunnels format
func TestYAMLParser(t *testing.T) {
converter := &Converter{}
tests := []struct {
name string
input string
expected *TunnelConfig
wantErr bool
}{
{
name: "valid_yaml_nested_format",
input: `tunnels:
HttpProxy:
type: httpclient
interface: 127.0.0.1
port: 4444
target: example.i2p
description: HTTP proxy tunnel
persistentKey: true
i2cp:
leaseSetEncType:
- "4"
- "0"
reduceIdleTime: 900000
options:
proxyList: "proxy1.i2p,proxy2.i2p"
sharedClient: true
inbound:
length: 3
outbound:
length: 2`,
expected: &TunnelConfig{
Name: "HttpProxy",
Type: "httpclient",
Interface: "127.0.0.1",
Port: 4444,
Target: "example.i2p",
Description: "HTTP proxy tunnel",
PersistentKey: true,
I2CP: map[string]interface{}{
"leaseSetEncType": []interface{}{"4", "0"},
"reduceIdleTime": 900000,
},
Tunnel: map[string]interface{}{
"proxyList": "proxy1.i2p,proxy2.i2p",
"sharedClient": true,
},
Inbound: map[string]interface{}{
"length": 3,
},
Outbound: map[string]interface{}{
"length": 2,
},
},
wantErr: false,
},
{
name: "minimal_yaml_tunnel",
input: `tunnels:
MinimalTunnel:
type: client`,
expected: &TunnelConfig{
Name: "MinimalTunnel",
Type: "client",
},
wantErr: false,
},
{
name: "invalid_yaml_syntax",
input: `tunnels:
BadTunnel:
type: httpclient
port: "not_a_number"
invalid_yaml: [
- missing closing bracket`,
expected: nil,
wantErr: true,
},
{
name: "empty_tunnels_map",
input: "tunnels: {}",
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := converter.parseYAML([]byte(tt.input))
if tt.wantErr {
if err == nil {
t.Errorf("parseYAML() expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("parseYAML() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.expected != nil {
if result.Name != tt.expected.Name {
t.Errorf("Name: expected %q, got %q", tt.expected.Name, result.Name)
}
if result.Type != tt.expected.Type {
t.Errorf("Type: expected %q, got %q", tt.expected.Type, result.Type)
}
if result.Interface != tt.expected.Interface {
t.Errorf("Interface: expected %q, got %q", tt.expected.Interface, result.Interface)
}
if result.Port != tt.expected.Port {
t.Errorf("Port: expected %d, got %d", tt.expected.Port, result.Port)
}
}
})
}
}
// TestConverterMethods tests the core Converter methods with 0% coverage
func TestConverterMethods(t *testing.T) {
converter := &Converter{strict: false}
t.Run("Convert_method", func(t *testing.T) {
// Test successful conversion
input := `name=TestConvert
type=httpclient
interface=127.0.0.1
listenPort=8080`
output, err := converter.Convert([]byte(input), "properties", "yaml")
if err != nil {
t.Fatalf("Convert() failed: %v", err)
}
if len(output) == 0 {
t.Error("Convert() returned empty output")
}
// Verify the output is valid YAML
_, err = converter.ParseInput(output, "yaml")
if err != nil {
t.Errorf("Convert() produced invalid YAML: %v", err)
}
})
t.Run("Convert_validation_error", func(t *testing.T) {
// Test conversion with invalid config (missing required fields)
input := `type=httpclient` // Missing name - should fail validation
_, err := converter.Convert([]byte(input), "properties", "yaml")
if err == nil {
t.Error("Convert() expected validation error, got nil")
}
// Verify it's a ValidationError
var validationErr *ValidationError
if !strings.Contains(err.Error(), "validation:") {
t.Errorf("Expected ValidationError, got: %v", err)
} else {
t.Log("ValidationError correctly returned")
}
// Test error unwrapping
if validationErr != nil && validationErr.Unwrap() == nil {
t.Error("ValidationError.Unwrap() returned nil")
}
})
t.Run("Convert_parse_error", func(t *testing.T) {
// Test conversion with malformed input that should cause parse error, not validation error
input := `malformed=properties=line=with=too=many=equals`
_, err := converter.Convert([]byte(input), "properties", "yaml")
if err == nil {
t.Error("Convert() expected error for malformed input, got nil")
}
// The error should occur during parsing/validation, which is expected behavior
t.Logf("Convert() correctly returned error for malformed input: %v", err)
})
t.Run("generateOutput_unsupported_format", func(t *testing.T) {
config := &TunnelConfig{
Name: "TestConfig",
Type: "httpclient",
}
_, err := converter.generateOutput(config, "unsupported")
if err == nil {
t.Error("generateOutput() expected error for unsupported format, got nil")
}
expectedErrMsg := "unsupported output format: unsupported"
if err.Error() != expectedErrMsg {
t.Errorf("generateOutput() error = %q, want %q", err.Error(), expectedErrMsg)
}
})
}
// TestErrorTypes tests the error type methods with 0% coverage
func TestErrorTypes(t *testing.T) {
t.Run("ConversionError", func(t *testing.T) {
innerErr := errors.New("test inner error")
convErr := &ConversionError{
Op: "test_operation",
Err: innerErr,
}
// Test Error() method
expectedMsg := "test_operation: test inner error"
if convErr.Error() != expectedMsg {
t.Errorf("ConversionError.Error() = %q, want %q", convErr.Error(), expectedMsg)
}
// Test Unwrap() method
unwrapped := convErr.Unwrap()
if unwrapped != innerErr {
t.Errorf("ConversionError.Unwrap() = %v, want %v", unwrapped, innerErr)
}
})
t.Run("ValidationError", func(t *testing.T) {
config := &TunnelConfig{Name: "TestConfig"}
innerErr := errors.New("test validation error")
valErr := &ValidationError{
Config: config,
Err: innerErr,
}
// Test Error() method
expectedMsg := "validation: test validation error"
if valErr.Error() != expectedMsg {
t.Errorf("ValidationError.Error() = %q, want %q", valErr.Error(), expectedMsg)
}
// Test Unwrap() method
unwrapped := valErr.Unwrap()
if unwrapped != innerErr {
t.Errorf("ValidationError.Unwrap() = %v, want %v", unwrapped, innerErr)
}
})
}
// TestTunnelConfigMethods tests TunnelConfig methods with 0% coverage
func TestTunnelConfigMethods(t *testing.T) {
t.Run("LoadConfig", func(t *testing.T) {
tempDir := t.TempDir()
// Test loading properties file
propsFile := filepath.Join(tempDir, "test.properties")
propsContent := `name=LoadTest
type=httpclient
interface=127.0.0.1
listenPort=8080
description=Load test tunnel`
err := os.WriteFile(propsFile, []byte(propsContent), 0644)
if err != nil {
t.Fatalf("Failed to create test properties file: %v", err)
}
config := &TunnelConfig{}
err = config.LoadConfig(propsFile)
if err != nil {
t.Fatalf("LoadConfig() failed: %v", err)
}
if config.Name != "LoadTest" {
t.Errorf("LoadConfig() Name = %q, want %q", config.Name, "LoadTest")
}
if config.Type != "httpclient" {
t.Errorf("LoadConfig() Type = %q, want %q", config.Type, "httpclient")
}
if config.Port != 8080 {
t.Errorf("LoadConfig() Port = %d, want %d", config.Port, 8080)
}
// Test loading YAML file (nested format with tunnels map)
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := `tunnels:
YamlTest:
type: server
port: 9090`
err = os.WriteFile(yamlFile, []byte(yamlContent), 0644)
if err != nil {
t.Fatalf("Failed to create test YAML file: %v", err)
}
config2 := &TunnelConfig{}
err = config2.LoadConfig(yamlFile)
if err != nil {
t.Fatalf("LoadConfig() YAML failed: %v", err)
}
if config2.Name != "YamlTest" {
t.Errorf("LoadConfig() YAML Name = %q, want %q", config2.Name, "YamlTest")
}
// Test loading non-existent file
config3 := &TunnelConfig{}
err = config3.LoadConfig("/non/existent/file")
if err == nil {
t.Error("LoadConfig() expected error for non-existent file, got nil")
}
})
t.Run("Options_and_SetOptions", func(t *testing.T) {
config := &TunnelConfig{
Name: "OptionsTest",
Type: "httpclient",
Interface: "192.168.1.100",
Port: 8080,
Target: "example.i2p",
PersistentKey: true,
Description: "Options test tunnel",
I2CP: map[string]interface{}{
"leaseSetEncType": "4,0",
},
Tunnel: map[string]interface{}{
"proxyList": "proxy1.i2p",
},
Inbound: map[string]interface{}{
"length": 3,
},
Outbound: map[string]interface{}{
"length": 2,
},
}
// Test Options() method
options := config.Options()
expectedOptions := map[string]string{
"Name": "OptionsTest",
"Type": "httpclient",
"Interface": "192.168.1.100",
"Port": "8080",
"Target": "example.i2p",
"PersistentKey": "true",
"Description": "Options test tunnel",
"I2CP.leaseSetEncType": "4,0",
"Tunnel.proxyList": "proxy1.i2p",
"Inbound.length": "3",
"Outbound.length": "2",
}
for key, expectedValue := range expectedOptions {
actualValue, exists := options[key]
if !exists {
t.Errorf("Options() missing key %q", key)
} else if actualValue != expectedValue {
t.Errorf("Options()[%q] = %q, want %q", key, actualValue, expectedValue)
}
}
// Test SetOptions() method
newConfig := &TunnelConfig{}
err := newConfig.SetOptions(options)
if err != nil {
t.Fatalf("SetOptions() failed: %v", err)
}
// Verify basic fields were set correctly
if newConfig.Name != config.Name {
t.Errorf("SetOptions() Name = %q, want %q", newConfig.Name, config.Name)
}
if newConfig.Type != config.Type {
t.Errorf("SetOptions() Type = %q, want %q", newConfig.Type, config.Type)
}
if newConfig.Port != config.Port {
t.Errorf("SetOptions() Port = %d, want %d", newConfig.Port, config.Port)
}
if newConfig.PersistentKey != config.PersistentKey {
t.Errorf("SetOptions() PersistentKey = %t, want %t", newConfig.PersistentKey, config.PersistentKey)
}
// Test SetOptions() with invalid port
invalidOptions := map[string]string{
"Name": "InvalidTest",
"Type": "client",
"Port": "invalid_port",
}
err = newConfig.SetOptions(invalidOptions)
if err == nil {
t.Error("SetOptions() expected error for invalid port, got nil")
}
// Test SetOptions() with invalid PersistentKey
invalidOptions2 := map[string]string{
"Name": "InvalidTest2",
"Type": "client",
"PersistentKey": "invalid_bool",
}
err = newConfig.SetOptions(invalidOptions2)
if err == nil {
t.Error("SetOptions() expected error for invalid PersistentKey, got nil")
}
})
}
// TestFormatDetection tests comprehensive format detection scenarios
func TestFormatDetection(t *testing.T) {
converter := &Converter{}
tests := []struct {
name string
filename string
expectedFormat string
expectError bool
}{
{
name: "properties_extension",
filename: "tunnel.properties",
expectedFormat: "properties",
expectError: false,
},
{
name: "config_extension",
filename: "tunnel.config",
expectedFormat: "properties",
expectError: false,
},
{
name: "prop_extension",
filename: "tunnel.prop",
expectedFormat: "properties",
expectError: false,
},
{
name: "yaml_extension",
filename: "tunnel.yaml",
expectedFormat: "yaml",
expectError: false,
},
{
name: "yml_extension",
filename: "tunnel.yml",
expectedFormat: "yaml",
expectError: false,
},
{
name: "ini_extension",
filename: "tunnel.ini",
expectedFormat: "ini",
expectError: false,
},
{
name: "conf_extension",
filename: "tunnel.conf",
expectedFormat: "ini",
expectError: false,
},
{
name: "unsupported_extension",
filename: "tunnel.xml",
expectError: true,
},
{
name: "no_extension",
filename: "tunnel",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
format, err := converter.DetectFormat(tt.filename)
if tt.expectError {
if err == nil {
t.Errorf("DetectFormat() expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("DetectFormat() error = %v, expectError %v", err, tt.expectError)
}
if format != tt.expectedFormat {
t.Errorf("DetectFormat() format = %q, want %q", format, tt.expectedFormat)
}
})
}
}
// TestINIFormatValidation tests the validateINIFormat function (0% coverage)
func TestINIFormatValidation(t *testing.T) {
tests := []struct {
name string
config *TunnelConfig
strict bool
expectError bool
}{
{
name: "valid_ini_name",
config: &TunnelConfig{
Name: "ValidTunnel",
Type: "httpclient",
Port: 8080,
},
strict: true,
expectError: false,
},
{
name: "ini_name_with_brackets_strict",
config: &TunnelConfig{
Name: "Bad[Tunnel]",
Type: "httpclient",
Port: 8080,
},
strict: true,
expectError: true,
},
{
name: "ini_name_with_brackets_non_strict",
config: &TunnelConfig{
Name: "Bad[Tunnel]",
Type: "httpclient",
Port: 8080,
},
strict: false,
expectError: true, // Basic validation should still catch this
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
validationCtx := NewValidationContext(tt.strict, "ini")
err := validationCtx.Validate(tt.config)
if tt.expectError {
if err == nil {
t.Errorf("Expected validation error for INI format, got nil")
}
} else {
if err != nil {
t.Errorf("Unexpected validation error: %v", err)
}
}
})
}
}