mirror of
https://github.com/go-i2p/go-sam-go.git
synced 2025-12-01 09:54:58 -05:00
Refactor tests to use local test listeners instead of external I2P sites for improved stability and reliability; add TestListener helper for managing local I2P listeners in tests.
This commit is contained in:
@@ -122,13 +122,13 @@ func TestDatagramSession_DialContext_Timeout(t *testing.T) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Microsecond)
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Microsecond)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
addr, err := session.sam.Lookup("idk.i2p")
|
// Create a test destination address instead of using external site
|
||||||
if err != nil {
|
testSAM2, testKeys2 := setupTestSAM(t)
|
||||||
t.Fatalf("Failed to lookup address: %v", err)
|
defer testSAM2.Close()
|
||||||
}
|
testAddr := testKeys2.Addr()
|
||||||
|
|
||||||
// Try to dial with short timeout
|
// Try to dial with short timeout
|
||||||
conn, err := session.DialContext(ctx, addr.Base64())
|
conn, err := session.DialContext(ctx, testAddr.Base64())
|
||||||
|
|
||||||
// Should get context deadline exceeded error
|
// Should get context deadline exceeded error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ func Test_PrimaryStreamingDial(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Println("Test_PrimaryStreamingDial")
|
fmt.Println("Test_PrimaryStreamingDial")
|
||||||
|
|
||||||
|
// Set up a local test listener instead of using external site
|
||||||
|
testListener := SetupTestListenerWithHTTP(t, generateUniqueSessionID("primary_streaming_dial_listener"))
|
||||||
|
defer testListener.Close()
|
||||||
|
|
||||||
earlysam, err := NewSAM(SAMDefaultAddr(""))
|
earlysam, err := NewSAM(SAMDefaultAddr(""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
@@ -49,16 +54,9 @@ func Test_PrimaryStreamingDial(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer ss.Close()
|
defer ss.Close()
|
||||||
fmt.Println("\tNotice: This may fail if your I2P node is not well integrated in the I2P network.")
|
fmt.Println("\tNotice: Using local test listener instead of external I2P site for improved test stability.")
|
||||||
fmt.Println("\tLooking up i2p-projekt.i2p")
|
fmt.Printf("\tDialing test listener (%s)\n", testListener.AddrString())
|
||||||
forumAddr, err := earlysam.Lookup("i2p-projekt.i2p")
|
conn, err := ss.DialI2P(testListener.Addr())
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
t.Fail()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println("\tDialing i2p-projekt.i2p(", forumAddr.Base32(), forumAddr.DestHash().Hash(), ")")
|
|
||||||
conn, err := ss.DialI2P(forumAddr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
t.Fail()
|
t.Fail()
|
||||||
@@ -74,9 +72,9 @@ func Test_PrimaryStreamingDial(t *testing.T) {
|
|||||||
buf := make([]byte, 4096)
|
buf := make([]byte, 4096)
|
||||||
n, err := conn.Read(buf)
|
n, err := conn.Read(buf)
|
||||||
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
|
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
|
||||||
fmt.Printf("\tProbably failed to StreamSession.DialI2P(i2p-projekt.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
|
fmt.Printf("\tProbably failed to StreamSession.DialI2P(test listener)? It replied %d bytes, but nothing that looked like http/html", n)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("\tRead HTTP/HTML from i2p-projekt.i2p")
|
fmt.Println("\tRead HTTP/HTML from test listener")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,19 @@ package stream
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// generateUniqueSessionID creates a unique session ID to prevent conflicts during concurrent test execution.
|
||||||
|
func generateUniqueSessionID(testName string) string {
|
||||||
|
timestamp := time.Now().UnixNano()
|
||||||
|
return fmt.Sprintf("%s_%d", testName, timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
func TestStreamSession_Dial(t *testing.T) {
|
func TestStreamSession_Dial(t *testing.T) {
|
||||||
sam, keys := setupTestSAM(t)
|
sam, keys := setupTestSAM(t)
|
||||||
defer sam.Close()
|
defer sam.Close()
|
||||||
@@ -18,14 +27,70 @@ func TestStreamSession_Dial(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
|
||||||
// Test dialing to a known I2P destination
|
// Create a local test listener instead of using external site
|
||||||
// This test might fail if the destination is not reachable
|
testSAM2, testKeys2 := setupTestSAM(t)
|
||||||
// but it tests the basic dial functionality
|
defer testSAM2.Close()
|
||||||
_, err = session.Dial("idk.i2p")
|
|
||||||
// We don't fail the test if dial fails since it depends on network conditions
|
listenerSession, err := NewStreamSession(testSAM2, generateUniqueSessionID("test_dial_listener"), testKeys2, []string{
|
||||||
// but we log it for debugging
|
"inbound.length=1", "outbound.length=1",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("Dial to idk.i2p failed (expected in some network conditions): %v", err)
|
t.Fatalf("Failed to create listener session: %v", err)
|
||||||
|
}
|
||||||
|
defer listenerSession.Close()
|
||||||
|
|
||||||
|
listener, err := listenerSession.Listen()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create listener: %v", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
// Start a simple echo server in background
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return // Listener closed
|
||||||
|
}
|
||||||
|
go func(c net.Conn) {
|
||||||
|
defer c.Close()
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
c.Read(buf) // Read the request
|
||||||
|
c.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body>Test response</body></html>"))
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Give listener time to be ready
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// Test dialing to the local listener
|
||||||
|
conn, err := session.Dial(listenerSession.Addr().Base32())
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Dial to local listener failed (might be expected due to I2P timing): %v", err)
|
||||||
|
return // Not a hard failure since I2P connections can be unreliable in test conditions
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Test basic communication
|
||||||
|
_, err = conn.Write([]byte("GET /\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Failed to write to connection: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Failed to read from connection: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := string(buf[:n])
|
||||||
|
if !strings.Contains(strings.ToLower(response), "html") {
|
||||||
|
t.Logf("Did not receive expected HTML response, got: %s", response)
|
||||||
|
} else {
|
||||||
|
t.Logf("Successfully received HTML response from local listener")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,16 +106,70 @@ func TestStreamSession_DialI2P(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
|
||||||
// Try to lookup a destination first
|
// Create a local test listener instead of using external site
|
||||||
addr, err := sam.Lookup("zzz.i2p")
|
testSAM2, testKeys2 := setupTestSAM(t)
|
||||||
|
defer testSAM2.Close()
|
||||||
|
|
||||||
|
listenerSession, err := NewStreamSession(testSAM2, generateUniqueSessionID("test_dial_i2p_listener"), testKeys2, []string{
|
||||||
|
"inbound.length=1", "outbound.length=1",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("Failed to lookup destination: %v", err)
|
t.Fatalf("Failed to create listener session: %v", err)
|
||||||
|
}
|
||||||
|
defer listenerSession.Close()
|
||||||
|
|
||||||
|
listener, err := listenerSession.Listen()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create listener: %v", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
// Start a simple echo server in background
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return // Listener closed
|
||||||
|
}
|
||||||
|
go func(c net.Conn) {
|
||||||
|
defer c.Close()
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
c.Read(buf) // Read the request
|
||||||
|
c.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body>Test response</body></html>"))
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Give listener time to be ready
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// Test dialing to the local listener using DialI2P with the actual I2P address
|
||||||
|
conn, err := session.DialI2P(listenerSession.Addr())
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("DialI2P to local listener failed (might be expected due to I2P timing): %v", err)
|
||||||
|
return // Not a hard failure since I2P connections can be unreliable in test conditions
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Test basic communication
|
||||||
|
_, err = conn.Write([]byte("GET /\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Failed to write to connection: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test dialing to the looked up address
|
buf := make([]byte, 1024)
|
||||||
_, err = session.DialI2P(addr)
|
n, err := conn.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("DialI2P failed (expected in some network conditions): %v", err)
|
t.Logf("Failed to read from connection: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := string(buf[:n])
|
||||||
|
if !strings.Contains(strings.ToLower(response), "html") {
|
||||||
|
t.Logf("Did not receive expected HTML response, got: %s", response)
|
||||||
|
} else {
|
||||||
|
t.Logf("Successfully received HTML response from local listener via DialI2P")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ func Test_StreamingDial(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Println("Test_StreamingDial")
|
fmt.Println("Test_StreamingDial")
|
||||||
|
|
||||||
|
// Set up a local test listener instead of using external site
|
||||||
|
testListener := SetupTestListenerWithHTTP(t, generateUniqueSessionID("streaming_dial_listener"))
|
||||||
|
defer testListener.Close()
|
||||||
|
|
||||||
sam, err := NewSAM(SAMDefaultAddr(""))
|
sam, err := NewSAM(SAMDefaultAddr(""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
@@ -44,16 +49,9 @@ func Test_StreamingDial(t *testing.T) {
|
|||||||
t.Fail()
|
t.Fail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Println("\tNotice: This may fail if your I2P node is not well integrated in the I2P network.")
|
fmt.Println("\tNotice: Using local test listener instead of external I2P site for improved test stability.")
|
||||||
fmt.Println("\tLooking up i2p-projekt.i2p")
|
fmt.Printf("\tDialing test listener (%s)\n", testListener.AddrString())
|
||||||
forumAddr, err := sam.Lookup("i2p-projekt.i2p")
|
conn, err := ss.DialI2P(testListener.Addr())
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
t.Fail()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println("\tDialing i2p-projekt.i2p(", forumAddr.Base32(), forumAddr.DestHash().Hash(), ")")
|
|
||||||
conn, err := ss.DialI2P(forumAddr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
t.Fail()
|
t.Fail()
|
||||||
@@ -69,9 +67,9 @@ func Test_StreamingDial(t *testing.T) {
|
|||||||
buf := make([]byte, 4096)
|
buf := make([]byte, 4096)
|
||||||
n, err := conn.Read(buf)
|
n, err := conn.Read(buf)
|
||||||
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
|
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
|
||||||
fmt.Printf("\tProbably failed to StreamSession.DialI2P(i2p-projekt.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
|
fmt.Printf("\tProbably failed to StreamSession.DialI2P(test listener)? It replied %d bytes, but nothing that looked like http/html", n)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("\tRead HTTP/HTML from i2p-projekt.i2p")
|
fmt.Println("\tRead HTTP/HTML from test listener")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +156,7 @@ func Test_StreamingServerClient(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ExampleStreamSession() {
|
func ExampleStreamSession() {
|
||||||
// Creates a new StreamingSession, dials to idk.i2p and gets a SAMConn
|
// Creates a new StreamingSession, dials to a local test listener and gets a SAMConn
|
||||||
// which behaves just like a normal net.Conn.
|
// which behaves just like a normal net.Conn.
|
||||||
//
|
//
|
||||||
// Requirements: This example requires a running I2P router with SAM bridge enabled.
|
// Requirements: This example requires a running I2P router with SAM bridge enabled.
|
||||||
@@ -182,15 +180,18 @@ func ExampleStreamSession() {
|
|||||||
fmt.Printf("Failed to create stream session: %v", err)
|
fmt.Printf("Failed to create stream session: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
someone, err := sam.Lookup("idk.i2p")
|
|
||||||
|
// Note: In a real example, you would set up a test listener here
|
||||||
|
// For demonstration purposes, we'll use a placeholder destination
|
||||||
|
someone, err := sam.Lookup("test.i2p")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to lookup idk.i2p: %v", err)
|
fmt.Printf("Failed to lookup test destination: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := ss.DialI2P(someone)
|
conn, err := ss.DialI2P(someone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to dial idk.i2p: %v", err)
|
fmt.Printf("Failed to dial test destination: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
@@ -206,17 +207,17 @@ func ExampleStreamSession() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
|
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
|
||||||
fmt.Printf("Failed to get HTTP/HTML response from idk.i2p (got %d bytes)", n)
|
fmt.Printf("Failed to get HTTP/HTML response from test destination (got %d bytes)", n)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Read HTTP/HTML from idk.i2p")
|
fmt.Println("Read HTTP/HTML from test destination")
|
||||||
log.Println("Read HTTP/HTML from idk.i2p")
|
log.Println("Read HTTP/HTML from test destination")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
// Sending HTTP GET /
|
// Sending HTTP GET /
|
||||||
// Read HTTP/HTML from idk.i2p
|
// Read HTTP/HTML from test destination
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleStreamListener() {
|
func ExampleStreamListener() {
|
||||||
|
|||||||
291
testhelpers.go
Normal file
291
testhelpers.go
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
package sam3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-i2p/i2pkeys"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestListener manages a local I2P listener for testing purposes.
|
||||||
|
// It provides a stable, local destination that can replace external sites in tests.
|
||||||
|
type TestListener struct {
|
||||||
|
sam *SAM
|
||||||
|
session *StreamSession
|
||||||
|
listener *StreamListener
|
||||||
|
addr i2pkeys.I2PAddr
|
||||||
|
closed bool
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestListenerConfig holds configuration for creating test listeners.
|
||||||
|
type TestListenerConfig struct {
|
||||||
|
SessionID string
|
||||||
|
HTTPResponse string // Optional custom HTTP response content
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTestListenerConfig returns a default configuration for test listeners.
|
||||||
|
func DefaultTestListenerConfig(sessionID string) *TestListenerConfig {
|
||||||
|
return &TestListenerConfig{
|
||||||
|
SessionID: sessionID,
|
||||||
|
HTTPResponse: "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body><h1>Test I2P Site</h1><p>This is a test response from a local I2P listener.</p></body></html>",
|
||||||
|
Timeout: 5 * time.Minute, // I2P tunnels can take time to establish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTestListener creates and starts a local I2P listener that can serve as a test destination.
|
||||||
|
// This replaces the need for external sites like i2p-projekt.i2p or idk.i2p in tests.
|
||||||
|
// The listener will respond to HTTP GET requests with basic HTML content.
|
||||||
|
func SetupTestListener(t *testing.T, config *TestListenerConfig) *TestListener {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
config = DefaultTestListenerConfig("test_listener")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SAM connection
|
||||||
|
sam, err := NewSAM(SAMDefaultAddr(""))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create SAM connection for test listener: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate keys for the listener
|
||||||
|
keys, err := sam.NewKeys()
|
||||||
|
if err != nil {
|
||||||
|
sam.Close()
|
||||||
|
t.Fatalf("Failed to generate keys for test listener: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create stream session with minimal 1-hop configuration for faster testing
|
||||||
|
session, err := sam.NewStreamSession(config.SessionID, keys, []string{
|
||||||
|
"inbound.length=1",
|
||||||
|
"outbound.length=1",
|
||||||
|
"inbound.lengthVariance=0",
|
||||||
|
"outbound.lengthVariance=0",
|
||||||
|
"inbound.quantity=1",
|
||||||
|
"outbound.quantity=1",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
sam.Close()
|
||||||
|
t.Fatalf("Failed to create stream session for test listener: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create listener
|
||||||
|
listener, err := session.Listen()
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
sam.Close()
|
||||||
|
t.Fatalf("Failed to create listener for test listener: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testListener := &TestListener{
|
||||||
|
sam: sam,
|
||||||
|
session: session,
|
||||||
|
listener: listener,
|
||||||
|
addr: keys.Addr(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start serving in background
|
||||||
|
go testListener.serve(t, config.HTTPResponse)
|
||||||
|
|
||||||
|
// Wait for listener to be ready with proper I2P timing
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := testListener.waitForReady(ctx, t); err != nil {
|
||||||
|
testListener.Close()
|
||||||
|
t.Fatalf("Test listener failed to become ready: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Test listener ready at %s", testListener.addr.Base32())
|
||||||
|
return testListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr returns the I2P address of the test listener.
|
||||||
|
func (tl *TestListener) Addr() i2pkeys.I2PAddr {
|
||||||
|
return tl.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddrString returns the Base32 address string of the test listener.
|
||||||
|
func (tl *TestListener) AddrString() string {
|
||||||
|
return tl.addr.Base32()
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve handles incoming connections to the test listener.
|
||||||
|
func (tl *TestListener) serve(t *testing.T, httpResponse string) {
|
||||||
|
for {
|
||||||
|
tl.mu.RLock()
|
||||||
|
if tl.closed {
|
||||||
|
tl.mu.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tl.mu.RUnlock()
|
||||||
|
|
||||||
|
conn, err := tl.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
tl.mu.RLock()
|
||||||
|
closed := tl.closed
|
||||||
|
tl.mu.RUnlock()
|
||||||
|
if !closed {
|
||||||
|
t.Logf("Test listener accept error: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle connection in goroutine to support multiple concurrent requests
|
||||||
|
go tl.handleConnection(conn, httpResponse, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleConnection processes a single connection to the test listener.
|
||||||
|
func (tl *TestListener) handleConnection(conn net.Conn, httpResponse string, t *testing.T) {
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Read the request (we expect HTTP GET)
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
t.Logf("Test listener read error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request := string(buf[:n])
|
||||||
|
t.Logf("Test listener received request: %s", strings.ReplaceAll(request, "\n", "\\n"))
|
||||||
|
|
||||||
|
// Send the configured HTTP response
|
||||||
|
_, err = conn.Write([]byte(httpResponse))
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Test listener write error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForReady waits for the test listener to be available for connections.
|
||||||
|
// This implements proper I2P timing considerations where tunnel establishment can take time.
|
||||||
|
func (tl *TestListener) waitForReady(ctx context.Context, t *testing.T) error {
|
||||||
|
// Create a test client to verify the listener is reachable
|
||||||
|
clientSAM, err := NewSAM(SAMDefaultAddr(""))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create test client SAM: %w", err)
|
||||||
|
}
|
||||||
|
defer clientSAM.Close()
|
||||||
|
|
||||||
|
clientKeys, err := clientSAM.NewKeys()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate test client keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSession, err := clientSAM.NewStreamSession("test_client_"+tl.session.ID(), clientKeys, []string{
|
||||||
|
"inbound.length=1",
|
||||||
|
"outbound.length=1",
|
||||||
|
"inbound.lengthVariance=0",
|
||||||
|
"outbound.lengthVariance=0",
|
||||||
|
"inbound.quantity=1",
|
||||||
|
"outbound.quantity=1",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create test client session: %w", err)
|
||||||
|
}
|
||||||
|
defer clientSession.Close()
|
||||||
|
|
||||||
|
// Try to connect with exponential backoff
|
||||||
|
backoff := 1 * time.Second
|
||||||
|
maxBackoff := 30 * time.Second
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("timeout waiting for test listener to be ready: %w", ctx.Err())
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Attempting to connect to test listener...")
|
||||||
|
conn, err := clientSession.DialI2P(tl.addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Test listener not ready yet: %v (retrying in %v)", err, backoff)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("timeout waiting for test listener to be ready: %w", ctx.Err())
|
||||||
|
case <-time.After(backoff):
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exponential backoff with jitter
|
||||||
|
backoff = backoff * 2
|
||||||
|
if backoff > maxBackoff {
|
||||||
|
backoff = maxBackoff
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successfully connected, verify basic communication
|
||||||
|
conn.Close()
|
||||||
|
t.Logf("Test listener is ready")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close shuts down the test listener and cleans up resources.
|
||||||
|
func (tl *TestListener) Close() error {
|
||||||
|
tl.mu.Lock()
|
||||||
|
defer tl.mu.Unlock()
|
||||||
|
|
||||||
|
if tl.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tl.closed = true
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if tl.listener != nil {
|
||||||
|
if err := tl.listener.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to close listener: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tl.session != nil {
|
||||||
|
if err := tl.session.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to close session: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tl.sam != nil {
|
||||||
|
if err := tl.sam.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to close SAM: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("multiple close errors: %v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTestListenerWithHTTP creates a test listener that provides HTTP-like responses
|
||||||
|
// suitable for replacing external web sites in tests.
|
||||||
|
func SetupTestListenerWithHTTP(t *testing.T, sessionID string) *TestListener {
|
||||||
|
config := &TestListenerConfig{
|
||||||
|
SessionID: sessionID,
|
||||||
|
HTTPResponse: "HTTP/1.1 200 OK\r\n" +
|
||||||
|
"Content-Type: text/html\r\n" +
|
||||||
|
"Content-Length: 120\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"<html><head><title>Test I2P Site</title></head>" +
|
||||||
|
"<body><h1>Hello from I2P!</h1><p>This is a test response.</p></body></html>",
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
return SetupTestListener(t, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateUniqueSessionID creates a unique session ID to prevent conflicts during concurrent test execution.
|
||||||
|
func generateUniqueSessionID(testName string) string {
|
||||||
|
timestamp := time.Now().UnixNano()
|
||||||
|
return fmt.Sprintf("%s_%d", testName, timestamp)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user