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