344 lines
8.4 KiB
Go
344 lines
8.4 KiB
Go
/*
|
|
* ============================================================================
|
|
* Projekt.....: rs2322tcp
|
|
* Datei.......: internal/client/application_test.go
|
|
* Copyright (C) 2026 Dieter Lang
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* Beschreibung:
|
|
* Tests für die Anwendungsschicht des rs2322tcp-Clients.
|
|
*
|
|
* Die Tests prüfen den Lebenszyklus der Application und die lokale
|
|
* Konfigurationsverwaltung, ohne dafür eine echte Serververbindung
|
|
* aufbauen zu müssen.
|
|
*
|
|
* Insbesondere wird sichergestellt, dass eine neu erzeugte Application
|
|
* ihre lokale Konfiguration bereits kennt, obwohl noch keine Verbindung
|
|
* zum Server besteht.
|
|
* ============================================================================
|
|
*/
|
|
|
|
package client
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.lang-dieter.de/rs2322tcp/internal/config"
|
|
)
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Test helpers
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// writeTestConfig creates a temporary client configuration file.
|
|
func writeTestConfig(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
dir := t.TempDir()
|
|
filename := filepath.Join(dir, "client.json")
|
|
|
|
content := `{
|
|
"server": {
|
|
"address": "127.0.0.1",
|
|
"port": 5000
|
|
},
|
|
"virtual_port_range": {
|
|
"first": 100,
|
|
"last": 199
|
|
},
|
|
"virtual_ports": [
|
|
{
|
|
"port": "/dev/ttyUSB100",
|
|
"remote_device": "radio"
|
|
},
|
|
{
|
|
"port": "/dev/ttyUSB101",
|
|
"remote_device": "rotor"
|
|
}
|
|
]
|
|
}`
|
|
|
|
if err := os.WriteFile(
|
|
filename,
|
|
[]byte(content),
|
|
0600,
|
|
); err != nil {
|
|
t.Fatalf("write test configuration: %v", err)
|
|
}
|
|
|
|
return filename
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// NewApplication
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// TestNewApplicationRejectsEmptyConfigFile verifies that an empty
|
|
// configuration-file path is rejected.
|
|
func TestNewApplicationRejectsEmptyConfigFile(t *testing.T) {
|
|
t.Helper()
|
|
|
|
application, err := NewApplication("")
|
|
if err == nil {
|
|
t.Fatal("NewApplication with empty config file returned no error")
|
|
}
|
|
|
|
if application != nil {
|
|
t.Fatal("NewApplication with empty config file returned an application")
|
|
}
|
|
}
|
|
|
|
// TestNewApplication verifies that a valid configuration-file path creates
|
|
// an application and loads the local configuration without starting the
|
|
// network connection.
|
|
func TestNewApplication(t *testing.T) {
|
|
t.Helper()
|
|
|
|
configFile := writeTestConfig(t)
|
|
|
|
application, err := NewApplication(configFile)
|
|
if err != nil {
|
|
t.Fatalf("NewApplication: %v", err)
|
|
}
|
|
|
|
if application == nil {
|
|
t.Fatal("NewApplication returned nil application")
|
|
}
|
|
|
|
if application.Running() {
|
|
t.Fatal("new application reports running")
|
|
}
|
|
|
|
cfg := application.Config()
|
|
if cfg == nil {
|
|
t.Fatal("new application returned no configuration")
|
|
}
|
|
|
|
if len(cfg.VirtualPorts) != 2 {
|
|
t.Fatalf(
|
|
"new application returned %d virtual ports, want 2",
|
|
len(cfg.VirtualPorts),
|
|
)
|
|
}
|
|
|
|
if cfg.VirtualPorts[0].Port != "/dev/ttyUSB100" {
|
|
t.Errorf(
|
|
"virtual port 0 = %q, want %q",
|
|
cfg.VirtualPorts[0].Port,
|
|
"/dev/ttyUSB100",
|
|
)
|
|
}
|
|
|
|
if cfg.VirtualPorts[0].RemoteDevice != "radio" {
|
|
t.Errorf(
|
|
"remote device 0 = %q, want %q",
|
|
cfg.VirtualPorts[0].RemoteDevice,
|
|
"radio",
|
|
)
|
|
}
|
|
|
|
if cfg.VirtualPorts[1].Port != "/dev/ttyUSB101" {
|
|
t.Errorf(
|
|
"virtual port 1 = %q, want %q",
|
|
cfg.VirtualPorts[1].Port,
|
|
"/dev/ttyUSB101",
|
|
)
|
|
}
|
|
|
|
if cfg.VirtualPorts[1].RemoteDevice != "rotor" {
|
|
t.Errorf(
|
|
"remote device 1 = %q, want %q",
|
|
cfg.VirtualPorts[1].RemoteDevice,
|
|
"rotor",
|
|
)
|
|
}
|
|
|
|
if devices := application.Devices(); len(devices) != 0 {
|
|
t.Fatalf(
|
|
"new application returned %d devices, want 0",
|
|
len(devices),
|
|
)
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Configuration copy
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// TestApplicationConfigReturnsCopy verifies that modifying the returned
|
|
// configuration does not modify the Application's internal configuration.
|
|
func TestApplicationConfigReturnsCopy(t *testing.T) {
|
|
t.Helper()
|
|
|
|
configFile := writeTestConfig(t)
|
|
|
|
application, err := NewApplication(configFile)
|
|
if err != nil {
|
|
t.Fatalf("NewApplication: %v", err)
|
|
}
|
|
|
|
cfg := application.Config()
|
|
if cfg == nil {
|
|
t.Fatal("Config returned nil")
|
|
}
|
|
|
|
cfg.VirtualPorts[0].Port = "/dev/ttyUSB999"
|
|
cfg.VirtualPorts[0].RemoteDevice = "changed"
|
|
|
|
current := application.Config()
|
|
if current == nil {
|
|
t.Fatal("second Config returned nil")
|
|
}
|
|
|
|
if current.VirtualPorts[0].Port != "/dev/ttyUSB100" {
|
|
t.Errorf(
|
|
"internal port changed to %q",
|
|
current.VirtualPorts[0].Port,
|
|
)
|
|
}
|
|
|
|
if current.VirtualPorts[0].RemoteDevice != "radio" {
|
|
t.Errorf(
|
|
"internal remote device changed to %q",
|
|
current.VirtualPorts[0].RemoteDevice,
|
|
)
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Close
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// TestApplicationCloseBeforeStart verifies that Close can safely be called
|
|
// before the application has been started and that the local configuration
|
|
// remains available.
|
|
func TestApplicationCloseBeforeStart(t *testing.T) {
|
|
t.Helper()
|
|
|
|
configFile := writeTestConfig(t)
|
|
|
|
application, err := NewApplication(configFile)
|
|
if err != nil {
|
|
t.Fatalf("NewApplication: %v", err)
|
|
}
|
|
|
|
if err := application.Close(); err != nil {
|
|
t.Fatalf("Close before Start: %v", err)
|
|
}
|
|
|
|
if application.Running() {
|
|
t.Fatal("application reports running after Close")
|
|
}
|
|
|
|
if cfg := application.Config(); cfg == nil {
|
|
t.Fatal("configuration was lost after Close")
|
|
}
|
|
}
|
|
|
|
// TestApplicationCloseIsIdempotent verifies that Close can be called more
|
|
// than once without producing an error and without losing the local
|
|
// configuration.
|
|
func TestApplicationCloseIsIdempotent(t *testing.T) {
|
|
t.Helper()
|
|
|
|
configFile := writeTestConfig(t)
|
|
|
|
application, err := NewApplication(configFile)
|
|
if err != nil {
|
|
t.Fatalf("NewApplication: %v", err)
|
|
}
|
|
|
|
if err := application.Close(); err != nil {
|
|
t.Fatalf("first Close: %v", err)
|
|
}
|
|
|
|
if err := application.Close(); err != nil {
|
|
t.Fatalf("second Close: %v", err)
|
|
}
|
|
|
|
if application.Running() {
|
|
t.Fatal("application reports running after repeated Close")
|
|
}
|
|
|
|
if cfg := application.Config(); cfg == nil {
|
|
t.Fatal("configuration was lost after repeated Close")
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Invalid configuration
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// TestNewApplicationRejectsInvalidConfiguration verifies that an invalid
|
|
// configuration file is rejected immediately.
|
|
func TestNewApplicationRejectsInvalidConfiguration(t *testing.T) {
|
|
t.Helper()
|
|
|
|
dir := t.TempDir()
|
|
filename := filepath.Join(dir, "invalid.json")
|
|
|
|
if err := os.WriteFile(
|
|
filename,
|
|
[]byte(`{"invalid": true}`),
|
|
0600,
|
|
); err != nil {
|
|
t.Fatalf("write invalid configuration: %v", err)
|
|
}
|
|
|
|
application, err := NewApplication(filename)
|
|
if err == nil {
|
|
t.Fatal("NewApplication with invalid configuration returned no error")
|
|
}
|
|
|
|
if application != nil {
|
|
t.Fatal(
|
|
"NewApplication with invalid configuration returned an application",
|
|
)
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Nil receiver
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// TestNilApplication verifies that the public lifecycle methods behave
|
|
// safely when called on a nil Application receiver.
|
|
//
|
|
// This is deliberately a small defensive test. The GUI should normally
|
|
// never operate on a nil Application.
|
|
func TestNilApplication(t *testing.T) {
|
|
t.Helper()
|
|
|
|
var application *Application
|
|
|
|
if err := application.Start(); err == nil {
|
|
t.Fatal("nil Application Start returned no error")
|
|
}
|
|
|
|
if err := application.Reconnect(); err == nil {
|
|
t.Fatal("nil Application Reconnect returned no error")
|
|
}
|
|
|
|
if err := application.Close(); err != nil {
|
|
t.Fatalf("nil Application Close: %v", err)
|
|
}
|
|
|
|
if application.Running() {
|
|
t.Fatal("nil Application reports running")
|
|
}
|
|
|
|
if application.Config() != nil {
|
|
t.Fatal("nil Application returned a configuration")
|
|
}
|
|
|
|
if devices := application.Devices(); devices != nil {
|
|
t.Fatal("nil Application returned devices")
|
|
}
|
|
}
|
|
|
|
// Keep config imported explicitly so the test documents that the temporary
|
|
// file represents the normal ClientConfig structure.
|
|
var _ config.ClientConfig
|