using System.Net; using System.Net.Sockets; using System.Text; namespace Nonemm.Spotting.Tests; /// The client driven through a real socket, so the telnet negotiation, the /// login and the spot parsing are exercised together. public class ClusterClientTests : IDisposable { private const byte Iac = 255; private const byte Wont = 252; private const byte Do = 253; private const byte TerminalType = 24; private static readonly TimeSpan Patience = TimeSpan.FromSeconds(5); private readonly TcpListener listener; public ClusterClientTests() { listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); } private int Port => ((IPEndPoint)listener.LocalEndpoint).Port; public void Dispose() => listener.Stop(); private ClusterClient Connect(IReadOnlyList? commands = null) => new("127.0.0.1", Port, "DL1ABC", commands, retryInterval: TimeSpan.FromMinutes(1)); private static async Task ReadAsync(NetworkStream stream, int bytes) { byte[] buffer = new byte[bytes]; int read = 0; while (read < bytes) { int got = await stream.ReadAsync(buffer.AsMemory(read)); if (got == 0) { break; } read += got; } return buffer[..read]; } private static async Task ReadTextAsync(NetworkStream stream, int bytes) => Encoding.Latin1.GetString(await ReadAsync(stream, bytes)); [Fact] public async Task TheCallsignGoesOutWhenTheNodeAsksForIt() { using ClusterClient cluster = Connect(); cluster.Start(); using TcpClient node = await listener.AcceptTcpClientAsync().WaitAsync(Patience); NetworkStream stream = node.GetStream(); await stream.WriteAsync(new byte[] { Iac, Do, TerminalType }); await stream.WriteAsync(Encoding.Latin1.GetBytes("login: ")); byte[] sent = await ReadAsync(stream, 3 + 8).WaitAsync(Patience); Assert.Equal(new byte[] { Iac, Wont, TerminalType }, sent[..3]); Assert.Equal("DL1ABC\r\n", Encoding.Latin1.GetString(sent[3..])); } [Fact] public async Task TheCommandsGoOutAfterTheLogin() { using ClusterClient cluster = Connect(["set/skimmer"]); cluster.Start(); using TcpClient node = await listener.AcceptTcpClientAsync().WaitAsync(Patience); NetworkStream stream = node.GetStream(); await stream.WriteAsync(Encoding.Latin1.GetBytes("Please enter your call: ")); Assert.Equal( "DL1ABC\r\nset/skimmer\r\n", await ReadTextAsync(stream, 8 + 13).WaitAsync(Patience)); } [Fact] public async Task ASpotFromTheNodeReachesTheBandmap() { using ClusterClient cluster = Connect(); TaskCompletionSource arrived = new(); cluster.SpotArrived += (_, spot) => arrived.TrySetResult(spot); cluster.Start(); using TcpClient node = await listener.AcceptTcpClientAsync().WaitAsync(Patience); await node.GetStream().WriteAsync(Encoding.Latin1.GetBytes( "DX de W3LPL: 14025.0 JA1XYZ CQ 1234Z\r\n")); Spot spot = await arrived.Task.WaitAsync(Patience); Assert.Equal("JA1XYZ", spot.Call.Text); Assert.Equal(14_025_000, spot.Frequency.Hertz); } [Fact] public async Task SpottingAStationSendsTheNodesDxCommand() { using ClusterClient cluster = Connect(); TaskCompletionSource connected = new(); cluster.ConnectionChanged += (_, up) => { if (up) { connected.TrySetResult(); } }; cluster.Start(); using TcpClient node = await listener.AcceptTcpClientAsync().WaitAsync(Patience); await connected.Task.WaitAsync(Patience); await cluster.SendSpotAsync( Core.Frequency.FromKilohertz(14_025), Core.Callsign.Parse("JA1XYZ"), "CQ"); Assert.Equal( "dx 14025.0 JA1XYZ CQ\r\n", await ReadTextAsync(node.GetStream(), 22).WaitAsync(Patience)); } }