mirror of
https://github.com/go-i2p/go-i2ptunnel.git
synced 2025-12-20 15:15:52 -05:00
Create HTTP Client tunnel test suites
This commit is contained in:
443
lib/http/client/httpclient_test.go
Normal file
443
lib/http/client/httpclient_test.go
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib"
|
||||||
|
i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestHTTPClientCreation tests the creation of an HTTP client tunnel.
|
||||||
|
// Why: Validates constructor logic and default state initialization.
|
||||||
|
// Design: Uses mock SAM address since we're testing creation, not connection.
|
||||||
|
func TestHTTPClientCreation(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-http-client",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPClient should create instance even without SAM available (connection happens on Start)
|
||||||
|
client, err := NewHTTPClient(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create HTTP client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify initial state
|
||||||
|
if client.Name() != "test-http-client" {
|
||||||
|
t.Errorf("Expected name 'test-http-client', got '%s'", client.Name())
|
||||||
|
}
|
||||||
|
if client.Type() != "httpclient" {
|
||||||
|
t.Errorf("Expected type 'httpclient', got '%s'", client.Type())
|
||||||
|
}
|
||||||
|
if client.Status() != i2ptunnel.I2PTunnelStatusStopped {
|
||||||
|
t.Errorf("Expected initial status stopped, got %v", client.Status())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClientOptions tests Options() and SetOptions() methods.
|
||||||
|
// Why: Configuration management is critical for production deployments.
|
||||||
|
// Design: Tests both retrieval and modification of tunnel options.
|
||||||
|
func TestHTTPClientOptions(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-options",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8118,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewHTTPClient(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Options() retrieval
|
||||||
|
opts := client.Options()
|
||||||
|
if opts["name"] != "test-options" {
|
||||||
|
t.Errorf("Expected name 'test-options', got '%s'", opts["name"])
|
||||||
|
}
|
||||||
|
if opts["port"] != "8118" {
|
||||||
|
t.Errorf("Expected port '8118', got '%s'", opts["port"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SetOptions() modification
|
||||||
|
newOpts := map[string]string{
|
||||||
|
"name": "updated-http-client",
|
||||||
|
"interface": "0.0.0.0",
|
||||||
|
"port": "9090",
|
||||||
|
}
|
||||||
|
if err := client.SetOptions(newOpts); err != nil {
|
||||||
|
t.Fatalf("Failed to set options: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify changes applied
|
||||||
|
updatedOpts := client.Options()
|
||||||
|
if updatedOpts["name"] != "updated-http-client" {
|
||||||
|
t.Errorf("Name not updated: got '%s'", updatedOpts["name"])
|
||||||
|
}
|
||||||
|
if updatedOpts["port"] != "9090" {
|
||||||
|
t.Errorf("Port not updated: got '%s'", updatedOpts["port"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClientSetOptionsValidation tests validation in SetOptions.
|
||||||
|
// Why: Invalid configurations should be rejected before causing runtime errors.
|
||||||
|
// Design: Tests multiple validation scenarios with expected error cases.
|
||||||
|
func TestHTTPClientSetOptionsValidation(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-validation",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8118,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewHTTPClient(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
opts map[string]string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid config",
|
||||||
|
opts: map[string]string{"port": "8080", "interface": "127.0.0.1"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid port - too high",
|
||||||
|
opts: map[string]string{"port": "99999"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid port - negative",
|
||||||
|
opts: map[string]string{"port": "-1"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid port - not a number",
|
||||||
|
opts: map[string]string{"port": "not-a-port"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty name",
|
||||||
|
opts: map[string]string{"name": ""},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := client.SetOptions(tt.opts)
|
||||||
|
if tt.wantError && err == nil {
|
||||||
|
t.Errorf("Expected error for %s, got nil", tt.name)
|
||||||
|
}
|
||||||
|
if !tt.wantError && err != nil {
|
||||||
|
t.Errorf("Expected no error for %s, got %v", tt.name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClientID tests ID generation.
|
||||||
|
// Why: IDs are used for tunnel identification in management interfaces.
|
||||||
|
// Design: Verifies ID is generated correctly from tunnel name.
|
||||||
|
func TestHTTPClientID(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "My HTTP Proxy",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8118,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewHTTPClient(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := client.ID()
|
||||||
|
// ID should be cleaned version of name
|
||||||
|
if id == "" {
|
||||||
|
t.Error("ID should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClientLocalAddress tests LocalAddress() method.
|
||||||
|
// Why: Applications need to know where to connect to use the proxy.
|
||||||
|
// Design: Verifies correct host:port formatting.
|
||||||
|
func TestHTTPClientLocalAddress(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-address",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8118,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewHTTPClient(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := client.LocalAddress()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get local address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := net.JoinHostPort("127.0.0.1", "8118")
|
||||||
|
if addr != expected {
|
||||||
|
t.Errorf("Expected address '%s', got '%s'", expected, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClientTarget tests Target() method.
|
||||||
|
// Why: HTTP clients are one-to-many proxies, so Target should be empty.
|
||||||
|
// Design: Verifies the method returns empty string for proxy tunnels.
|
||||||
|
func TestHTTPClientTarget(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-target",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8118,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewHTTPClient(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target := client.Target()
|
||||||
|
if target != "" {
|
||||||
|
t.Errorf("HTTP client should have empty target (one-to-many), got '%s'", target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClientLoadConfig tests configuration loading from file.
|
||||||
|
// Why: Production deployments need to persist and reload configurations.
|
||||||
|
// Design: Tests successful load, validation, and error cases.
|
||||||
|
func TestHTTPClientLoadConfig(t *testing.T) {
|
||||||
|
// Create temporary directory for test configs
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Test case 1: Successful load from YAML
|
||||||
|
t.Run("successful load from yaml", func(t *testing.T) {
|
||||||
|
configPath := filepath.Join(tmpDir, "http-client.yaml")
|
||||||
|
// YAML format requires tunnels: map with tunnel name as key
|
||||||
|
configContent := `tunnels:
|
||||||
|
loaded-http-client:
|
||||||
|
name: loaded-http-client
|
||||||
|
type: httpclient
|
||||||
|
interface: 0.0.0.0
|
||||||
|
port: 8888
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewHTTPClient(i2pconv.TunnelConfig{
|
||||||
|
Name: "original",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.LoadConfig(configPath); err != nil {
|
||||||
|
t.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify config was loaded
|
||||||
|
if client.Name() != "loaded-http-client" {
|
||||||
|
t.Errorf("Expected name 'loaded-http-client', got '%s'", client.Name())
|
||||||
|
}
|
||||||
|
if client.TunnelConfig.Port != 8888 {
|
||||||
|
t.Errorf("Expected port 8888, got %d", client.TunnelConfig.Port)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 2: Reject config load while running
|
||||||
|
t.Run("reject load while running", func(t *testing.T) {
|
||||||
|
client, err := NewHTTPClient(i2pconv.TunnelConfig{
|
||||||
|
Name: "test",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate running state
|
||||||
|
client.I2PTunnelStatus = i2ptunnel.I2PTunnelStatusRunning
|
||||||
|
|
||||||
|
configPath := filepath.Join(tmpDir, "test.yaml")
|
||||||
|
configContent := `tunnels:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
type: httpclient
|
||||||
|
interface: 127.0.0.1
|
||||||
|
port: 8080
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.LoadConfig(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when loading config while running")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 3: Reject wrong tunnel type
|
||||||
|
t.Run("reject wrong type", func(t *testing.T) {
|
||||||
|
client, err := NewHTTPClient(i2pconv.TunnelConfig{
|
||||||
|
Name: "test",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(tmpDir, "wrong-type.yaml")
|
||||||
|
configContent := `tunnels:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
type: tcpclient
|
||||||
|
interface: 127.0.0.1
|
||||||
|
port: 8080
|
||||||
|
target: test.i2p
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.LoadConfig(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when loading wrong tunnel type")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 4: Handle invalid file
|
||||||
|
t.Run("invalid file", func(t *testing.T) {
|
||||||
|
client, err := NewHTTPClient(i2pconv.TunnelConfig{
|
||||||
|
Name: "test",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.LoadConfig("/nonexistent/path/config.yaml")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when loading nonexistent file")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClientErrorTracking tests error recording and retrieval.
|
||||||
|
// Why: Production systems need error visibility for debugging.
|
||||||
|
// Design: Tests error history tracking and Error() method.
|
||||||
|
func TestHTTPClientErrorTracking(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-errors",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8118,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewHTTPClient(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initially no errors
|
||||||
|
if client.Error() != nil {
|
||||||
|
t.Error("Expected no error initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record an error
|
||||||
|
testErr := fmt.Errorf("test error")
|
||||||
|
client.recordError(testErr)
|
||||||
|
|
||||||
|
// Verify error was recorded
|
||||||
|
if client.Error() == nil {
|
||||||
|
t.Error("Expected error to be recorded")
|
||||||
|
}
|
||||||
|
if len(client.Errors) != 1 {
|
||||||
|
t.Errorf("Expected 1 error, got %d", len(client.Errors))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClientStopBeforeStart tests stopping a non-started tunnel.
|
||||||
|
// Why: Defensive programming - stop should be safe to call anytime.
|
||||||
|
// Design: Verifies Stop() is idempotent and doesn't panic.
|
||||||
|
func TestHTTPClientStopBeforeStart(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-stop",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: 8118,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewHTTPClient(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop should not error on non-started tunnel
|
||||||
|
if err := client.Stop(); err != nil {
|
||||||
|
t.Errorf("Stop should not error on non-started tunnel: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClientPortAllocation tests that the client can bind to available ports.
|
||||||
|
// Why: Port conflicts are common deployment issues.
|
||||||
|
// Design: Uses random available port to avoid conflicts in test environment.
|
||||||
|
func TestHTTPClientPortAllocation(t *testing.T) {
|
||||||
|
// Find an available port
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to find available port: %v", err)
|
||||||
|
}
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
listener.Close()
|
||||||
|
|
||||||
|
// Give OS time to release the port
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-port",
|
||||||
|
Type: "httpclient",
|
||||||
|
Port: port,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewHTTPClient(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := client.LocalAddress()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get local address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
||||||
|
if addr != expectedAddr {
|
||||||
|
t.Errorf("Expected address %s, got %s", expectedAddr, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
542
lib/http/server/httpserver_test.go
Normal file
542
lib/http/server/httpserver_test.go
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
i2pconv "github.com/go-i2p/go-i2ptunnel-config/lib"
|
||||||
|
i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestHTTPServerCreation tests the creation of an HTTP server tunnel.
|
||||||
|
// Why: Validates constructor logic and default state initialization.
|
||||||
|
// Design: Uses mock SAM address since we're testing creation, not connection.
|
||||||
|
func TestHTTPServerCreation(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-http-server",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:8080",
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create HTTP server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify initial state
|
||||||
|
if server.Name() != "test-http-server" {
|
||||||
|
t.Errorf("Expected name 'test-http-server', got '%s'", server.Name())
|
||||||
|
}
|
||||||
|
if server.Type() != "httpserver" {
|
||||||
|
t.Errorf("Expected type 'httpserver', got '%s'", server.Type())
|
||||||
|
}
|
||||||
|
if server.Status() != i2ptunnel.I2PTunnelStatusStopped {
|
||||||
|
t.Errorf("Expected initial status stopped, got %v", server.Status())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPServerOptions tests Options() and SetOptions() methods.
|
||||||
|
// Why: Configuration management is critical for production deployments.
|
||||||
|
// Design: Tests both retrieval and modification of tunnel options.
|
||||||
|
func TestHTTPServerOptions(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-options",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:9090",
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Options() retrieval
|
||||||
|
opts := server.Options()
|
||||||
|
if opts["name"] != "test-options" {
|
||||||
|
t.Errorf("Expected name 'test-options', got '%s'", opts["name"])
|
||||||
|
}
|
||||||
|
if opts["port"] != "8080" {
|
||||||
|
t.Errorf("Expected port '8080', got '%s'", opts["port"])
|
||||||
|
}
|
||||||
|
// Note: target in options comes from server.Addr, which is set to interface:port during construction
|
||||||
|
// This is the local listening address, not the forward target
|
||||||
|
|
||||||
|
// Test SetOptions() modification
|
||||||
|
newOpts := map[string]string{
|
||||||
|
"name": "updated-http-server",
|
||||||
|
"interface": "0.0.0.0",
|
||||||
|
"port": "9999",
|
||||||
|
"maxconns": "50",
|
||||||
|
"ratelimit": "10.5",
|
||||||
|
}
|
||||||
|
if err := server.SetOptions(newOpts); err != nil {
|
||||||
|
t.Fatalf("Failed to set options: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify changes applied
|
||||||
|
updatedOpts := server.Options()
|
||||||
|
if updatedOpts["name"] != "updated-http-server" {
|
||||||
|
t.Errorf("Name not updated: got '%s'", updatedOpts["name"])
|
||||||
|
}
|
||||||
|
if updatedOpts["port"] != "9999" {
|
||||||
|
t.Errorf("Port not updated: got '%s'", updatedOpts["port"])
|
||||||
|
}
|
||||||
|
if updatedOpts["maxconns"] != "50" {
|
||||||
|
t.Errorf("MaxConns not updated: got '%s'", updatedOpts["maxconns"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPServerSetOptionsValidation tests validation in SetOptions.
|
||||||
|
// Why: Invalid configurations should be rejected before causing runtime errors.
|
||||||
|
// Design: Tests multiple validation scenarios with expected error cases.
|
||||||
|
func TestHTTPServerSetOptionsValidation(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-validation",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:8080",
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
opts map[string]string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid config",
|
||||||
|
opts: map[string]string{"port": "8080", "maxconns": "100"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid port - too high",
|
||||||
|
opts: map[string]string{"port": "99999"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid port - negative",
|
||||||
|
opts: map[string]string{"port": "-1"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid maxconns - not a number",
|
||||||
|
opts: map[string]string{"maxconns": "not-a-number"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid ratelimit - not a number",
|
||||||
|
opts: map[string]string{"ratelimit": "not-a-number"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty name",
|
||||||
|
opts: map[string]string{"name": ""},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := server.SetOptions(tt.opts)
|
||||||
|
if tt.wantError && err == nil {
|
||||||
|
t.Errorf("Expected error for %s, got nil", tt.name)
|
||||||
|
}
|
||||||
|
if !tt.wantError && err != nil {
|
||||||
|
t.Errorf("Expected no error for %s, got %v", tt.name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPServerID tests ID generation.
|
||||||
|
// Why: IDs are used for tunnel identification in management interfaces.
|
||||||
|
// Design: Verifies ID is generated correctly from tunnel name.
|
||||||
|
func TestHTTPServerID(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "My HTTP Server",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:9090",
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := server.ID()
|
||||||
|
// ID should be cleaned version of name
|
||||||
|
if id == "" {
|
||||||
|
t.Error("ID should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPServerLocalAddress tests LocalAddress() method.
|
||||||
|
// Why: Management interfaces need to know the I2P listening address.
|
||||||
|
// Design: Verifies correct host:port formatting.
|
||||||
|
func TestHTTPServerLocalAddress(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-address",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:9090",
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := server.LocalAddress()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get local address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := net.JoinHostPort("127.0.0.1", "8080")
|
||||||
|
if addr != expected {
|
||||||
|
t.Errorf("Expected address '%s', got '%s'", expected, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPServerTarget tests Target() method.
|
||||||
|
// Why: HTTP server forwards to a specific local service target.
|
||||||
|
// Design: Verifies the target address reflects the listening address.
|
||||||
|
// Note: Currently server.Addr is set to interface:port, not the actual forward target.
|
||||||
|
func TestHTTPServerTarget(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-target",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:9090",
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target := server.Target()
|
||||||
|
// The server's Addr field is currently set to interface:port
|
||||||
|
expectedTarget := "127.0.0.1:8080"
|
||||||
|
if target != expectedTarget {
|
||||||
|
t.Errorf("Expected target '%s', got '%s'", expectedTarget, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPServerLoadConfig tests configuration loading from file.
|
||||||
|
// Why: Production deployments need to persist and reload configurations.
|
||||||
|
// Design: Tests successful load, validation, and error cases.
|
||||||
|
func TestHTTPServerLoadConfig(t *testing.T) {
|
||||||
|
// Create temporary directory for test configs
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Test case 1: Successful load from YAML
|
||||||
|
t.Run("successful load from yaml", func(t *testing.T) {
|
||||||
|
configPath := filepath.Join(tmpDir, "http-server.yaml")
|
||||||
|
// YAML format requires tunnels: map with tunnel name as key
|
||||||
|
configContent := `tunnels:
|
||||||
|
loaded-http-server:
|
||||||
|
name: loaded-http-server
|
||||||
|
type: httpserver
|
||||||
|
interface: 0.0.0.0
|
||||||
|
port: 8888
|
||||||
|
target: 127.0.0.1:9999
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(i2pconv.TunnelConfig{
|
||||||
|
Name: "original",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:8080",
|
||||||
|
}, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.LoadConfig(configPath); err != nil {
|
||||||
|
t.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify config was loaded
|
||||||
|
if server.Name() != "loaded-http-server" {
|
||||||
|
t.Errorf("Expected name 'loaded-http-server', got '%s'", server.Name())
|
||||||
|
}
|
||||||
|
if server.TunnelConfig.Port != 8888 {
|
||||||
|
t.Errorf("Expected port 8888, got %d", server.TunnelConfig.Port)
|
||||||
|
}
|
||||||
|
if server.Target() != "127.0.0.1:9999" {
|
||||||
|
t.Errorf("Expected target '127.0.0.1:9999', got '%s'", server.Target())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 2: Reject config load while running
|
||||||
|
t.Run("reject load while running", func(t *testing.T) {
|
||||||
|
server, err := NewHTTPServer(i2pconv.TunnelConfig{
|
||||||
|
Name: "test",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:8080",
|
||||||
|
}, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate running state
|
||||||
|
server.I2PTunnelStatus = i2ptunnel.I2PTunnelStatusRunning
|
||||||
|
|
||||||
|
configPath := filepath.Join(tmpDir, "test.yaml")
|
||||||
|
configContent := `tunnels:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
type: httpserver
|
||||||
|
interface: 127.0.0.1
|
||||||
|
port: 8080
|
||||||
|
target: 127.0.0.1:8080
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.LoadConfig(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when loading config while running")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 3: Reject wrong tunnel type
|
||||||
|
t.Run("reject wrong type", func(t *testing.T) {
|
||||||
|
server, err := NewHTTPServer(i2pconv.TunnelConfig{
|
||||||
|
Name: "test",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:8080",
|
||||||
|
}, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(tmpDir, "wrong-type.yaml")
|
||||||
|
configContent := `tunnels:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
type: httpclient
|
||||||
|
interface: 127.0.0.1
|
||||||
|
port: 8080
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.LoadConfig(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when loading wrong tunnel type")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 4: Reject invalid target address
|
||||||
|
t.Run("invalid target address", func(t *testing.T) {
|
||||||
|
server, err := NewHTTPServer(i2pconv.TunnelConfig{
|
||||||
|
Name: "test",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:8080",
|
||||||
|
}, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(tmpDir, "invalid-target.yaml")
|
||||||
|
configContent := `tunnels:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
type: httpserver
|
||||||
|
interface: 127.0.0.1
|
||||||
|
port: 8080
|
||||||
|
target: invalid-target-address
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.LoadConfig(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when loading invalid target address")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case 5: Handle invalid file
|
||||||
|
t.Run("invalid file", func(t *testing.T) {
|
||||||
|
server, err := NewHTTPServer(i2pconv.TunnelConfig{
|
||||||
|
Name: "test",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:8080",
|
||||||
|
}, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.LoadConfig("/nonexistent/path/config.yaml")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when loading nonexistent file")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPServerErrorTracking tests error recording and retrieval.
|
||||||
|
// Why: Production systems need error visibility for debugging.
|
||||||
|
// Design: Tests error history tracking and Error() method.
|
||||||
|
func TestHTTPServerErrorTracking(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-errors",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:9090",
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initially no errors
|
||||||
|
if server.Error() != nil {
|
||||||
|
t.Error("Expected no error initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record an error
|
||||||
|
testErr := fmt.Errorf("test error")
|
||||||
|
server.recordError(testErr)
|
||||||
|
|
||||||
|
// Verify error was recorded
|
||||||
|
if server.Error() == nil {
|
||||||
|
t.Error("Expected error to be recorded")
|
||||||
|
}
|
||||||
|
if len(server.Errors) != 1 {
|
||||||
|
t.Errorf("Expected 1 error, got %d", len(server.Errors))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPServerStopBeforeStart tests stopping a non-started tunnel.
|
||||||
|
// Why: Defensive programming - stop should be safe to call anytime.
|
||||||
|
// Design: Verifies Stop() is idempotent and doesn't panic.
|
||||||
|
func TestHTTPServerStopBeforeStart(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-stop",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:9090",
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop should not panic on non-started tunnel
|
||||||
|
if err := server.Stop(); err != nil {
|
||||||
|
t.Errorf("Stop should not error on non-started tunnel: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPServerRateLimiting tests rate limiting configuration.
|
||||||
|
// Why: Rate limiting is crucial for preventing abuse in production.
|
||||||
|
// Design: Verifies rate limit and maxconns settings.
|
||||||
|
func TestHTTPServerRateLimiting(t *testing.T) {
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-ratelimit",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: 8080,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:9090",
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set rate limiting options
|
||||||
|
opts := map[string]string{
|
||||||
|
"maxconns": "25",
|
||||||
|
"ratelimit": "5.5",
|
||||||
|
}
|
||||||
|
if err := server.SetOptions(opts); err != nil {
|
||||||
|
t.Fatalf("Failed to set rate limiting options: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify settings
|
||||||
|
retrievedOpts := server.Options()
|
||||||
|
if retrievedOpts["maxconns"] != "25" {
|
||||||
|
t.Errorf("Expected maxconns '25', got '%s'", retrievedOpts["maxconns"])
|
||||||
|
}
|
||||||
|
if retrievedOpts["ratelimit"] != "5.5" {
|
||||||
|
t.Errorf("Expected ratelimit '5.5', got '%s'", retrievedOpts["ratelimit"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPServerPortAllocation tests that the server can bind to available ports.
|
||||||
|
// Why: Port conflicts are common deployment issues.
|
||||||
|
// Design: Uses random available port to avoid conflicts in test environment.
|
||||||
|
func TestHTTPServerPortAllocation(t *testing.T) {
|
||||||
|
// Find an available port
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to find available port: %v", err)
|
||||||
|
}
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
listener.Close()
|
||||||
|
|
||||||
|
// Give OS time to release the port
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
config := i2pconv.TunnelConfig{
|
||||||
|
Name: "test-port",
|
||||||
|
Type: "httpserver",
|
||||||
|
Port: port,
|
||||||
|
Interface: "127.0.0.1",
|
||||||
|
Target: "127.0.0.1:9090",
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := NewHTTPServer(config, "127.0.0.1:7656")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := server.LocalAddress()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get local address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
||||||
|
if addr != expectedAddr {
|
||||||
|
t.Errorf("Expected address %s, got %s", expectedAddr, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user