443 lines
8.9 KiB
Go
443 lines
8.9 KiB
Go
/*
|
|
* ============================================================================
|
|
* Projekt.....: rs2322tcp
|
|
* Datei.......: internal/client/connection_test.go
|
|
* Copyright (C) 2026 Dieter Lang
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* Beschreibung:
|
|
* Tests für den Aufbau einer Verbindung zwischen einem lokalen virtuellen
|
|
* seriellen Port und dem zugehörigen entfernten TCP-Gerät.
|
|
*
|
|
* Die Tests prüfen insbesondere die Auswahl des Remote-Geräts, die Erzeugung
|
|
* des virtuellen Ports, den Aufbau der TCP-Datenverbindung und die Verbindung
|
|
* dieser Endpunkte über eine Bridge.
|
|
* ============================================================================
|
|
*/
|
|
package client
|
|
|
|
import (
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.lang-dieter.de/rs2322tcp/internal/config"
|
|
"git.lang-dieter.de/rs2322tcp/internal/transport"
|
|
)
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Test connection
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// testRemoteAddrConn wraps a net.Conn and provides a TCP-style remote address.
|
|
//
|
|
// net.Pipe() is used for the control connection in these tests. Its default
|
|
// RemoteAddr() is "pipe", which cannot be used by OpenDataConnection() to
|
|
// determine the server host. This wrapper supplies a normal TCP address.
|
|
type testRemoteAddrConn struct {
|
|
net.Conn
|
|
remoteAddr net.Addr
|
|
}
|
|
|
|
// RemoteAddr returns the TCP-style remote address configured for the test.
|
|
func (c *testRemoteAddrConn) RemoteAddr() net.Addr {
|
|
return c.remoteAddr
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Test helpers
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// startTestDataServer starts a local TCP listener for a simulated remote
|
|
// device.
|
|
//
|
|
// The listener accepts exactly one connection. The accepted connection is
|
|
// returned through the channel so the test can verify that the client has
|
|
// successfully established the data connection.
|
|
func startTestDataServer(t *testing.T) (string, <-chan net.Conn) {
|
|
t.Helper()
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen for test data server: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
_ = listener.Close()
|
|
})
|
|
|
|
accepted := make(chan net.Conn, 1)
|
|
|
|
go func() {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
accepted <- conn
|
|
}()
|
|
|
|
return listener.Addr().String(), accepted
|
|
}
|
|
|
|
// remoteDeviceFromAddress creates a RemoteDeviceInfo whose DataPort is
|
|
// taken from the supplied local TCP address.
|
|
func remoteDeviceFromAddress(
|
|
t *testing.T,
|
|
id string,
|
|
address string,
|
|
) transport.RemoteDeviceInfo {
|
|
t.Helper()
|
|
|
|
_, portText, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
t.Fatalf("split test server address %q: %v", address, err)
|
|
}
|
|
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
t.Fatalf("parse test server port %q: %v", portText, err)
|
|
}
|
|
|
|
return transport.RemoteDeviceInfo{
|
|
ID: id,
|
|
Name: id,
|
|
DataPort: port,
|
|
}
|
|
}
|
|
|
|
// newTestClient creates a Client whose control connection uses net.Pipe()
|
|
// but reports a normal TCP-style remote address.
|
|
func newTestClient(t *testing.T) *Client {
|
|
t.Helper()
|
|
|
|
controlClient, serverConn := net.Pipe()
|
|
|
|
t.Cleanup(func() {
|
|
_ = controlClient.Close()
|
|
_ = serverConn.Close()
|
|
})
|
|
|
|
return &Client{
|
|
conn: &testRemoteAddrConn{
|
|
Conn: controlClient,
|
|
remoteAddr: &net.TCPAddr{
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
Port: 5000,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Tests
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
func TestConnectVirtualPortNilClient(t *testing.T) {
|
|
manager := NewVirtualPortManager(
|
|
config.VirtualPortRangeConfig{
|
|
First: 100,
|
|
Last: 199,
|
|
},
|
|
)
|
|
|
|
cfg := config.VirtualPortConfig{
|
|
Port: "/dev/ttyUSB100",
|
|
RemoteDevice: "radio",
|
|
}
|
|
|
|
devices := []transport.RemoteDeviceInfo{
|
|
{
|
|
ID: "radio",
|
|
DataPort: 50123,
|
|
},
|
|
}
|
|
|
|
_, _, err := connectVirtualPort(
|
|
nil,
|
|
manager,
|
|
cfg,
|
|
devices,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected error for nil client")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "client is not connected") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestConnectVirtualPortNilManager(t *testing.T) {
|
|
cfg := config.VirtualPortConfig{
|
|
Port: "/dev/ttyUSB100",
|
|
RemoteDevice: "radio",
|
|
}
|
|
|
|
devices := []transport.RemoteDeviceInfo{
|
|
{
|
|
ID: "radio",
|
|
DataPort: 50123,
|
|
},
|
|
}
|
|
|
|
_, _, err := connectVirtualPort(
|
|
&Client{},
|
|
nil,
|
|
cfg,
|
|
devices,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected error for nil manager")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "client is not connected") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestConnectVirtualPortUnknownDevice(t *testing.T) {
|
|
manager := NewVirtualPortManager(
|
|
config.VirtualPortRangeConfig{
|
|
First: 100,
|
|
Last: 199,
|
|
},
|
|
)
|
|
|
|
client := newTestClient(t)
|
|
|
|
cfg := config.VirtualPortConfig{
|
|
Port: "/dev/ttyUSB100",
|
|
RemoteDevice: "unknown",
|
|
}
|
|
|
|
devices := []transport.RemoteDeviceInfo{
|
|
{
|
|
ID: "radio",
|
|
DataPort: 50123,
|
|
},
|
|
}
|
|
|
|
_, _, err := connectVirtualPort(
|
|
client,
|
|
manager,
|
|
cfg,
|
|
devices,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown device")
|
|
}
|
|
|
|
if !strings.Contains(
|
|
err.Error(),
|
|
`remote device "unknown" not found`,
|
|
) {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestConnectVirtualPortClosesPortWhenDataConnectionFails(t *testing.T) {
|
|
manager := NewVirtualPortManager(
|
|
config.VirtualPortRangeConfig{
|
|
First: 100,
|
|
Last: 100,
|
|
},
|
|
)
|
|
|
|
client := newTestClient(t)
|
|
|
|
cfg := config.VirtualPortConfig{
|
|
Port: "/dev/ttyUSB100",
|
|
RemoteDevice: "radio",
|
|
}
|
|
|
|
devices := []transport.RemoteDeviceInfo{
|
|
{
|
|
ID: "radio",
|
|
DataPort: 1,
|
|
},
|
|
}
|
|
|
|
port, conn, err := connectVirtualPort(
|
|
client,
|
|
manager,
|
|
cfg,
|
|
devices,
|
|
)
|
|
|
|
if err == nil {
|
|
if conn != nil {
|
|
_ = conn.Close()
|
|
}
|
|
|
|
if port != nil {
|
|
_ = port.Close()
|
|
}
|
|
|
|
t.Fatal("expected data connection error")
|
|
}
|
|
|
|
if port != nil {
|
|
t.Fatal(
|
|
"virtual port must not be returned after connection failure",
|
|
)
|
|
}
|
|
|
|
if conn != nil {
|
|
t.Fatal(
|
|
"TCP connection must not be returned after connection failure",
|
|
)
|
|
}
|
|
|
|
// Port 100 must have been released after the failed connection.
|
|
portNumber, err := manager.Reserve()
|
|
if err != nil {
|
|
t.Fatalf("Reserve after cleanup: %v", err)
|
|
}
|
|
|
|
if portNumber != 100 {
|
|
t.Fatalf("reserved port = %d, want 100", portNumber)
|
|
}
|
|
|
|
manager.Release(portNumber)
|
|
}
|
|
|
|
func TestConnectVirtualPortSuccess(t *testing.T) {
|
|
dataAddress, accepted := startTestDataServer(t)
|
|
|
|
device := remoteDeviceFromAddress(
|
|
t,
|
|
"radio",
|
|
dataAddress,
|
|
)
|
|
|
|
client := newTestClient(t)
|
|
|
|
manager := NewVirtualPortManager(
|
|
config.VirtualPortRangeConfig{
|
|
First: 100,
|
|
Last: 199,
|
|
},
|
|
)
|
|
|
|
cfg := config.VirtualPortConfig{
|
|
Port: "/dev/ttyUSB100",
|
|
RemoteDevice: "radio",
|
|
}
|
|
|
|
virtualPort, dataConn, err := connectVirtualPort(
|
|
client,
|
|
manager,
|
|
cfg,
|
|
[]transport.RemoteDeviceInfo{device},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("connectVirtualPort: %v", err)
|
|
}
|
|
|
|
if virtualPort == nil {
|
|
t.Fatal("virtual port is nil")
|
|
}
|
|
|
|
if dataConn == nil {
|
|
t.Fatal("data connection is nil")
|
|
}
|
|
|
|
defer virtualPort.Close()
|
|
defer dataConn.Close()
|
|
|
|
if virtualPort.Path() != "/dev/ttyUSB100" {
|
|
t.Fatalf(
|
|
"virtual port path = %q, want %q",
|
|
virtualPort.Path(),
|
|
"/dev/ttyUSB100",
|
|
)
|
|
}
|
|
|
|
// The test data server must receive the connection created by
|
|
// OpenDataConnection().
|
|
select {
|
|
case conn := <-accepted:
|
|
_ = conn.Close()
|
|
|
|
case <-t.Context().Done():
|
|
t.Fatal("test context cancelled while waiting for data connection")
|
|
}
|
|
}
|
|
|
|
func TestConnectBridgeSuccess(t *testing.T) {
|
|
dataAddress, accepted := startTestDataServer(t)
|
|
|
|
device := remoteDeviceFromAddress(
|
|
t,
|
|
"radio",
|
|
dataAddress,
|
|
)
|
|
|
|
client := newTestClient(t)
|
|
|
|
manager := NewVirtualPortManager(
|
|
config.VirtualPortRangeConfig{
|
|
First: 100,
|
|
Last: 199,
|
|
},
|
|
)
|
|
|
|
cfg := config.VirtualPortConfig{
|
|
Port: "/dev/ttyUSB100",
|
|
RemoteDevice: "radio",
|
|
}
|
|
|
|
bridge, virtualPort, dataConn, err := connectBridge(
|
|
client,
|
|
manager,
|
|
cfg,
|
|
[]transport.RemoteDeviceInfo{device},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("connectBridge: %v", err)
|
|
}
|
|
|
|
if bridge == nil {
|
|
t.Fatal("bridge is nil")
|
|
}
|
|
|
|
if virtualPort == nil {
|
|
t.Fatal("virtual port is nil")
|
|
}
|
|
|
|
if dataConn == nil {
|
|
t.Fatal("data connection is nil")
|
|
}
|
|
|
|
defer virtualPort.Close()
|
|
defer dataConn.Close()
|
|
|
|
if bridge.serial != virtualPort {
|
|
t.Fatal("bridge serial endpoint does not match virtual port")
|
|
}
|
|
|
|
if bridge.conn != dataConn {
|
|
t.Fatal("bridge TCP endpoint does not match data connection")
|
|
}
|
|
|
|
if virtualPort.Path() != "/dev/ttyUSB100" {
|
|
t.Fatalf(
|
|
"virtual port path = %q, want %q",
|
|
virtualPort.Path(),
|
|
"/dev/ttyUSB100",
|
|
)
|
|
}
|
|
|
|
// The test data server must receive the TCP data connection created by
|
|
// connectBridge().
|
|
select {
|
|
case conn := <-accepted:
|
|
_ = conn.Close()
|
|
|
|
case <-t.Context().Done():
|
|
t.Fatal("test context cancelled while waiting for data connection")
|
|
}
|
|
}
|