using System.Net; using System.Net.Sockets; using Nonemm.Core; namespace Nonemm.Network.Tests; /// Two links talking to each other over loopback. The stations are named by /// hand rather than found by a beacon: a broadcast test would depend on the /// machine's interfaces, and what is being tested here is what happens after /// two stations have found each other. public class StationLinkTests { private const string Version = "1.0.11364"; private static readonly TimeSpan Patience = TimeSpan.FromSeconds(10); [Fact] public async Task AContactReachesTheOtherStation() { (int onePort, int twoPort) = SparePorts(); using StationLink one = new("RUN-PC", Version, stationNumber: 1, port: onePort); using StationLink two = new("MULT-PC", Version, stationNumber: 2, port: twoPort); one.Start(); two.Start(); ContactUpdate? arrived = null; two.UpdateArrived += (_, update) => arrived = update; one.AddStation("MULT-PC", "127.0.0.1", twoPort); await WaitForAsync(() => one.Stations.Any(s => s is { IsMine: false, IsConnected: true })); await one.SendLoggedAsync(Contact()); await WaitForAsync(() => arrived is not null); ContactLogged logged = Assert.IsType(arrived); Assert.Equal("G3XYZ", logged.Qso.Call.Text); Assert.Equal("RUN-PC", logged.Qso.StationName); Assert.Equal(1, logged.Qso.NetworkedComputerNumber); } /// The first message on a new connection says which station of the entry /// the sender is, so the other end can show its number. [Fact] public async Task ConnectingSaysWhichStationNumberThisIs() { (int onePort, int twoPort) = SparePorts(); using StationLink one = new("RUN-PC", Version, stationNumber: 1, port: onePort); using StationLink two = new("MULT-PC", Version, stationNumber: 7, port: twoPort); one.Start(); two.Start(); one.AddStation("MULT-PC", "127.0.0.1", twoPort); await WaitForAsync(() => two.Stations.FirstOrDefault(s => s.ComputerName == "RUN-PC")?.StationNumber == 1); NetworkedStation? seen = two.Stations.FirstOrDefault(s => s.ComputerName == "RUN-PC"); Assert.Equal(1, seen?.StationNumber); Assert.Equal("IAM", seen?.LastMessage); } /// The band a station is on is what the status window shows and what the /// band-change rule counts. [Fact] public async Task WhereAStationIsReachesTheOthers() { (int onePort, int twoPort) = SparePorts(); using StationLink one = new("RUN-PC", Version, stationNumber: 1, port: onePort); using StationLink two = new("MULT-PC", Version, stationNumber: 2, port: twoPort); one.Start(); two.Start(); one.AddStation("MULT-PC", "127.0.0.1", twoPort); await WaitForAsync(() => one.Stations.Any(s => s is { IsMine: false, IsConnected: true })); await one.SendBandAsync(Frequency.FromKilohertz(21_025), Modes.Cw, running: true, radioNumber: 1); await WaitForAsync(() => two.Stations.FirstOrDefault(s => s.ComputerName == "RUN-PC")?.Band?.Name == "15M"); NetworkedStation? seen = two.Stations.FirstOrDefault(s => s.ComputerName == "RUN-PC"); Assert.Equal("15M", seen?.Band?.Name); Assert.Equal("CW", seen?.Mode?.Name); Assert.True(seen?.IsRunning); } [Fact] public async Task ChatReachesTheOtherOperator() { (int onePort, int twoPort) = SparePorts(); using StationLink one = new("RUN-PC", Version, stationNumber: 1, port: onePort); using StationLink two = new("MULT-PC", Version, stationNumber: 2, port: twoPort); one.Start(); two.Start(); string? said = null; two.TalkArrived += (_, text) => said = text; one.AddStation("MULT-PC", "127.0.0.1", twoPort); await WaitForAsync(() => one.Stations.Any(s => s is { IsMine: false, IsConnected: true })); await one.SendTalkAsync("qsy 20"); await WaitForAsync(() => said is not null); Assert.Equal("[RUN-PC] qsy 20", said); } /// An echo request is answered without anybody asking, which is how N1MM /// measures the round trip to each station. [Fact] public async Task AnEchoRequestIsAnswered() { (int onePort, int twoPort) = SparePorts(); using StationLink one = new("RUN-PC", Version, stationNumber: 1, port: onePort); using StationLink two = new("MULT-PC", Version, stationNumber: 2, port: twoPort); one.Start(); two.Start(); one.AddStation("MULT-PC", "127.0.0.1", twoPort); two.AddStation("RUN-PC", "127.0.0.1", onePort); await WaitForAsync(() => one.Stations.Any(s => s is { IsMine: false, IsConnected: true }) && two.Stations.Any(s => s is { IsMine: false, IsConnected: true })); await one.SendEchoRequestAsync(); await WaitForAsync(() => one.Stations.FirstOrDefault(s => s.ComputerName == "MULT-PC")?.EchoTime is not null); Assert.NotNull(one.Stations.FirstOrDefault(s => s.ComputerName == "MULT-PC")?.EchoTime); } /// This computer is in its own list, the way N1MM shows it, so an operator /// can read its station number and version off the same window. [Fact] public void ThisComputerIsInItsOwnStationList() { using StationLink link = new("RUN-PC", Version, stationNumber: 3, port: SparePorts().One); NetworkedStation mine = Assert.Single(link.Stations); Assert.True(mine.IsMine); Assert.Equal("RUN-PC", mine.ComputerName); Assert.Equal(3, mine.StationNumber); Assert.Equal(Version, mine.Version); } /// A message that reached nobody says so, rather than looking as though it /// went out. [Fact] public async Task SendingWithNoStationsConnectedReachesNobody() { using StationLink link = new("RUN-PC", Version, port: SparePorts().One); link.Start(); Assert.Equal(0, await link.SendLoggedAsync(Contact())); } private static async Task WaitForAsync(Func ready) { DateTime giveUp = DateTime.UtcNow + Patience; while (!ready() && DateTime.UtcNow < giveUp) { await Task.Delay(5); } } /// Two ports nothing else is on, so the tests do not fight each other or /// the N1MM the machine may be running. /// /// Both are taken before either is let go. Asking twice in a row does not /// work: the second ask often gets the port the first one has just given /// back, and two links on one port leaves the second without a listener. private static (int One, int Two) SparePorts() { TcpListener first = new(IPAddress.Loopback, 0); TcpListener second = new(IPAddress.Loopback, 0); first.Start(); second.Start(); int one = ((IPEndPoint)first.LocalEndpoint).Port; int two = ((IPEndPoint)second.LocalEndpoint).Port; first.Stop(); second.Stop(); return (one, two); } private static Qso Contact() => new() { Id = Qso.NewId(), TimestampUtc = new DateTime(2026, 9, 3, 12, 34, 56, DateTimeKind.Utc), Call = Callsign.Parse("G3XYZ"), Frequency = Frequency.FromKilohertz(14_025), Mode = Modes.Cw, ContestName = "CQWW", ContestNumber = 2, SentNumber = 41, }; }