/* * ============================================================================ * Projekt.....: rs2322tcp * Datei.......: data_connection_test.go * Copyright (C) 2026 Dieter Lang * * SPDX-License-Identifier: GPL-3.0-or-later * * Beschreibung: * Tests für die bidirektionale TCP-/RS232-Datenverbindung. * ============================================================================ */ package server_test import ( "bytes" "errors" "io" "net" "sync" "testing" "time" "git.lang-dieter.de/rs2322tcp/internal/server" ) /////////////////////////////////////////////////////////////////////////////// // Test serial connection /////////////////////////////////////////////////////////////////////////////// type testSerialConnection struct { reader *bytes.Reader writer bytes.Buffer writeErr error mu sync.Mutex closed bool closeCh chan struct{} closeOnce sync.Once } func newTestSerialConnection(data []byte) *testSerialConnection { return &testSerialConnection{ reader: bytes.NewReader(data), closeCh: make(chan struct{}), } } func newFailingTestSerialConnection(writeErr error) *testSerialConnection { return &testSerialConnection{ reader: bytes.NewReader(nil), writeErr: writeErr, closeCh: make(chan struct{}), } } func (s *testSerialConnection) Read(p []byte) (int, error) { s.mu.Lock() if s.closed { s.mu.Unlock() return 0, io.ErrClosedPipe } n, err := s.reader.Read(p) s.mu.Unlock() if err != io.EOF { return n, err } // A real serial connection normally waits for further data instead // of immediately returning EOF. Block until the connection is closed. <-s.closeCh return 0, io.ErrClosedPipe } func (s *testSerialConnection) Write(p []byte) (int, error) { s.mu.Lock() defer s.mu.Unlock() if s.closed { return 0, io.ErrClosedPipe } if s.writeErr != nil { return 0, s.writeErr } return s.writer.Write(p) } func (s *testSerialConnection) Close() error { s.closeOnce.Do(func() { s.mu.Lock() s.closed = true s.mu.Unlock() close(s.closeCh) }) return nil } func (s *testSerialConnection) WrittenData() []byte { s.mu.Lock() defer s.mu.Unlock() data := make([]byte, s.writer.Len()) copy(data, s.writer.Bytes()) return data } /////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////// func TestNewDataConnection(t *testing.T) { tcpServer, tcpClient := net.Pipe() defer tcpServer.Close() defer tcpClient.Close() serial := newTestSerialConnection(nil) connection, err := server.NewDataConnection( tcpServer, serial, testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataConnection() failed: %v", err) } if connection.TCPConn() == nil { t.Fatal("TCPConn() returned nil") } if connection.SerialConn() == nil { t.Fatal("SerialConn() returned nil") } } func TestNewDataConnectionRejectsNilTCP(t *testing.T) { serial := newTestSerialConnection(nil) connection, err := server.NewDataConnection( nil, serial, testHardwareErrorResponse, ) if err == nil { t.Fatal("NewDataConnection() succeeded, want error") } if connection != nil { t.Fatal("NewDataConnection() returned connection despite error") } } func TestNewDataConnectionRejectsNilSerial(t *testing.T) { tcpServer, tcpClient := net.Pipe() defer tcpServer.Close() defer tcpClient.Close() connection, err := server.NewDataConnection( tcpServer, nil, testHardwareErrorResponse, ) if err == nil { t.Fatal("NewDataConnection() succeeded, want error") } if connection != nil { t.Fatal("NewDataConnection() returned connection despite error") } } /////////////////////////////////////////////////////////////////////////////// // TCP -> Serial /////////////////////////////////////////////////////////////////////////////// func TestDataConnectionTCPToSerial(t *testing.T) { tcpServer, tcpClient := net.Pipe() defer tcpClient.Close() serial := newTestSerialConnection(nil) connection, err := server.NewDataConnection( tcpServer, serial, testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataConnection() failed: %v", err) } done := make(chan error, 1) go func() { done <- connection.Run() }() testData := []byte("hello serial") if _, err := tcpClient.Write(testData); err != nil { t.Fatalf("TCP Write() failed: %v", err) } deadline := time.Now().Add(time.Second) for { if bytes.Equal(serial.WrittenData(), testData) { break } if time.Now().After(deadline) { t.Fatalf( "serial data = %q, want %q", serial.WrittenData(), testData, ) } time.Sleep(time.Millisecond) } _ = tcpClient.Close() select { case <-done: case <-time.After(time.Second): t.Fatal("DataConnection.Run() did not terminate") } } /////////////////////////////////////////////////////////////////////////////// // TCP -> Serial error /////////////////////////////////////////////////////////////////////////////// func TestDataConnectionTCPToSerialWriteError(t *testing.T) { tcpServer, tcpClient := net.Pipe() defer tcpClient.Close() writeErr := errors.New("hardware write failed") serial := newFailingTestSerialConnection(writeErr) connection, err := server.NewDataConnection( tcpServer, serial, testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataConnection() failed: %v", err) } done := make(chan error, 1) go func() { done <- connection.Run() }() testData := []byte("hello unavailable hardware") if _, err := tcpClient.Write(testData); err != nil { t.Fatalf("TCP Write() failed: %v", err) } errorResponse := make([]byte, len(testHardwareErrorResponse)) if _, err := io.ReadFull(tcpClient, errorResponse); err != nil { t.Fatalf("TCP Read() failed: %v", err) } if string(errorResponse) != testHardwareErrorResponse { t.Fatalf( "error response = %q, want %q", string(errorResponse), testHardwareErrorResponse, ) } select { case <-done: case <-time.After(time.Second): t.Fatal("DataConnection.Run() did not terminate") } } /////////////////////////////////////////////////////////////////////////////// // Serial -> TCP /////////////////////////////////////////////////////////////////////////////// func TestDataConnectionSerialToTCP(t *testing.T) { tcpServer, tcpClient := net.Pipe() defer tcpClient.Close() testData := []byte("hello tcp") serial := newTestSerialConnection(testData) connection, err := server.NewDataConnection( tcpServer, serial, testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataConnection() failed: %v", err) } done := make(chan error, 1) go func() { done <- connection.Run() }() buffer := make([]byte, len(testData)) if _, err := io.ReadFull(tcpClient, buffer); err != nil { t.Fatalf("TCP Read() failed: %v", err) } if !bytes.Equal(buffer, testData) { t.Fatalf( "TCP data = %q, want %q", buffer, testData, ) } _ = tcpClient.Close() select { case <-done: case <-time.After(time.Second): t.Fatal("DataConnection.Run() did not terminate") } } /////////////////////////////////////////////////////////////////////////////// // Close /////////////////////////////////////////////////////////////////////////////// func TestDataConnectionClose(t *testing.T) { tcpServer, tcpClient := net.Pipe() defer tcpClient.Close() serial := newTestSerialConnection(nil) connection, err := server.NewDataConnection( tcpServer, serial, testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataConnection() failed: %v", err) } if err := connection.Close(); err != nil { t.Fatalf("Close() failed: %v", err) } if err := connection.Close(); err != nil { t.Fatalf("second Close() failed: %v", err) } buffer := make([]byte, 1) if _, err := tcpClient.Read(buffer); err == nil { t.Fatal("TCP connection is still open") } serial.mu.Lock() closed := serial.closed serial.mu.Unlock() if !closed { t.Fatal("serial connection is still open") } }