Comprehensive testing documentation for socksgo.
Tag compattest needed to run all tests.
go test ./... -tags=compattestThe testhooks build tag enables test hooks for improved coverage of error paths:
# Run tests with test hooks enabled
go test ./... -tags="compattest testhooks"
# Run tests with race detector and test hooks
go test -race -tags="compattest testhooks" ./...The test hooks allow testing of error paths that are difficult to trigger through normal API usage, such as:
LookupIPwith FQDN response (triggersErrWrongAddrInLookupResponse)Listenwith smux server errors- Connection close behavior on error paths
Test hooks are only compiled when the testhooks tag is used and have zero impact on production builds.
Internal test files and test hook code are excluded from coverage reports to avoid inflating coverage metrics with test-only code that is not part of the production build.
Excluded files:
*_internal_test.go- Internal test files for package-private functionsclient_testhooks.go- Test hooks for error path testing (build tagtesthooks)
Generate coverage report (excluding internal tests):
# Generate coverage profile
go test ./... -tags="compattest testhooks" -coverprofile=coverage.out -coverpkg=./...
# Filter using the helper script
./scripts/filter_coverage.sh
# Or manually:
grep -v "client_testhooks.go" coverage.out | \
grep -v "_internal_test.go" > coverage_filtered.out && \
mv coverage_filtered.out coverage.out
# View coverage report
go tool cover -func=coverage.out
# Generate HTML report
go tool cover -html=coverage.out -o coverage.htmlCI automatically filters internal test files and testhooks from coverage before submitting to Coveralls.
Example usage in tests:
//go:build testhooks
package socksgo_test
import (
"testing"
"github.com/asciimoth/socksgo"
"github.com/asciimoth/socksgo/protocol"
)
func TestLookupIPErrorPath(t *testing.T) {
// Set up hook to simulate FQDN response
oldHook := socksgo.GetTestLookupIPHook()
socksgo.SetTestLookupIPHook(func(addr protocol.Addr) protocol.Addr {
return protocol.AddrFromFQDN("example.com", 80, "tcp")
})
defer socksgo.SetTestLookupIPHook(oldHook)
// ... test code ...
}Available hooks:
SetTestLookupIPHook(hook)/GetTestLookupIPHook()SetTestLookupAddrHook(hook)/GetTestLookupAddrHook()SetTestListenSmuxHook(hook)/GetTestListenSmuxHook()SetTestListenCloseHook(hook)/GetTestListenCloseHook()ResetTestHooks()- Reset all hooks to defaults
socksgo/
├── client_config_test.go # Client configuration tests
├── client_test.go # Client operation tests
├── common_test.go # Shared test utilities
├── compat_curl_test.go # curl compatibility tests
├── compat_gost_test.go # gost compatibility tests
├── compat_tor_test.go # Tor compatibility tests
├── compat_test.go # General compatibility tests
├── errors_test.go # Error type tests
├── pair_client_server_test.go # Client-server integration tests
├── server_config_test.go # Server configuration tests
├── server_handler_*_test.go # Individual handler tests
│ ├── server_handler_assoc_test.go
│ ├── server_handler_bind_test.go
│ ├── server_handler_connect_test.go
│ ├── server_handler_mbind_test.go
│ ├── server_handler_resolve_test.go
│ ├── server_handler_resolveptr_test.go
│ └── server_handler_tun_test.go
├── server_handlers_test.go # Handler registry tests
├── server_test.go # Server operation tests
│
└── protocol/ # Protocol layer tests
├── addr_test.go
├── cmd_test.go
├── errors_test.go
├── helpers_test.go
├── pipe_test.go
├── reply_test.go
├── v4_test.go
├── v5auth_gss_test.go
├── v5auth_pass_test.go
├── v5auth_test.go
├── v5tcp_test.go
└── v5udp_test.go
Test individual functions and methods in isolation.
Example:
func TestBuildFilter_HostsAndWildcards(t *testing.T) {
t.Parallel()
filter := socksgo.BuildFilter("*.example.com,192.168.1.1")
tests := []struct {
address string
expected bool
}{
{"test.example.com:80", true},
{"192.168.1.1:80", true},
{"google.com:80", false},
}
for _, tt := range tests {
t.Run(tt.address, func(t *testing.T) {
result := filter("", tt.address)
if result != tt.expected {
t.Errorf("got %v, want %v", result, tt.expected)
}
})
}
}Test client-server interactions with actual network connections.
Example:
func TestClientServerConnect(t *testing.T) {
t.Parallel()
// Start test server
listener := startTestServer(t)
defer listener.Close()
// Create client
client := &socksgo.Client{
ProxyAddr: listener.Addr().String(),
}
// Test CONNECT through proxy
conn, err := client.Dial(context.Background(), "tcp", "example.com:80")
if err != nil {
t.Fatal(err)
}
defer conn.Close()
// Verify connection works
_, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"))
if err != nil {
t.Fatal(err)
}
}Test with external SOCKS implementations.
Example:
func TestCompatGost(t *testing.T) {
t.Parallel()
// Start gost server
gostCmd := startGostServer(t)
defer gostCmd.Process.Kill()
// Test our client against gost server
client := &socksgo.Client{
ProxyAddr: "127.0.0.1:1080",
GostMbind: true,
}
conn, err := client.Dial(context.Background(), "tcp", "example.com:80")
if err != nil {
t.Fatal(err)
}
conn.Close()
}Use table-driven tests for multiple scenarios.
Example:
func TestClient_Version_GetNet_GetAddr(t *testing.T) {
t.Parallel()
tests := []struct {
name string
client *socksgo.Client
expectedVer string
expectedNet string
expectedAddr string
}{
{
name: "default",
client: &socksgo.Client{},
expectedVer: "5",
expectedNet: "tcp",
expectedAddr: "",
},
{
name: "socks4",
client: &socksgo.Client{
SocksVersion: "4",
ProxyAddr: "127.0.0.1:1080",
},
expectedVer: "4",
expectedNet: "tcp",
expectedAddr: "127.0.0.1:1080",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := tt.client.Version(); got != tt.expectedVer {
t.Errorf("Version() = %v, want %v", got, tt.expectedVer)
}
})
}
}-
Client Configuration
ClientFromURL,ClientFromURLObjSafe- Filter building and matching
- Version detection
-
Protocol Encoding/Decoding
- SOCKS4 request/reply
- SOCKS5 request/reply
- Address type conversions
- Auth method negotiation
-
Basic Client-Server Operations
- CONNECT command (all SOCKS versions)
- TLS transport
- WebSocket transport
- Username/password auth
-
Error Types
- All error type string formatting
- Error wrapping and unwrapping
-
Server Handlers
- Happy path tested
- Error paths need more coverage
- Edge cases (timeouts, cancellations)
-
UDP Operations
- Basic UDP ASSOC tested
- Gost UDPTun needs more tests
- UDP timeout behavior
-
Extensions
- MBIND basic functionality
- Tor resolve commands
- Extension flag interactions
-
Server Error Handling
PreCmdhook rejection scenarios- Address filter rejections
- Auth failure handling in server
- Resource cleanup on errors
-
Concurrency
- Race conditions in UDP proxy
- MBIND session management
- Concurrent handler execution
-
Timeout Scenarios
- Handshake timeout
- UDP assoc timeout
- Context cancellation propagation
-
Gost Extension Details
- UDPTun fragmentation handling
- MBIND stream errors
- Extension negotiation