using System.Net; using System.Net.Sockets; using System.Text; using System.Xml.Linq; namespace Nonemm.Network.Tests; public class BroadcastTests { [Fact] public void An_address_without_a_port_takes_the_default_port() { IReadOnlyList? parsed = BroadcastTargets.Parse("127.0.0.1 192.168.1.255:9000", 12060, out _); Assert.Equal([new IPEndPoint(IPAddress.Loopback, 12060), new IPEndPoint(IPAddress.Parse("192.168.1.255"), 9000)], parsed); } [Theory] [InlineData("not-an-address")] [InlineData("127.0.0.1:99999")] [InlineData("127.0.0.1:12060:1")] public void An_entry_that_is_not_an_address_is_refused(string text) { Assert.Null(BroadcastTargets.Parse(text, 12060, out string problem)); Assert.Contains(text, problem); } [Fact] public void The_score_and_the_rotor_have_their_own_default_ports() { Assert.Equal(12050, BroadcastTargets.DefaultFor(BroadcastKind.Score).Port); Assert.Equal(12040, BroadcastTargets.DefaultFor(BroadcastKind.Rotor).Port); Assert.Equal(12060, BroadcastTargets.DefaultFor(BroadcastKind.Radio).Port); } [Fact] public void Radio_info_carries_the_frequency_in_units_of_ten_hertz() { string xml = BroadcastMessages.Radio( new RadioReport(1, 14_025_500, 14_025_500, "CW", "OK1ABC", "OK1ABC", true, 1, false, 1, false, "", true), "RUN-PC"); XElement root = XElement.Parse(xml); Assert.Equal("RadioInfo", root.Name.LocalName); Assert.Equal("1402550", root.Element("Freq")!.Value); Assert.Equal("True", root.Element("IsRunning")!.Value); Assert.Equal("RUN-PC", root.Element("StationName")!.Value); } [Fact] public void A_radio_message_is_read_back_in_hertz() { string xml = BroadcastMessages.Radio( new RadioReport(2, 14_025_500, 14_030_000, "USB", "OK1ABC", "OK1ABC", true, 2, true, 2, false, "", true), "RUN-PC"); RadioBroadcast? read = BroadcastMessages.ReadRadio(xml); Assert.Equal("RUN-PC", read!.StationName); Assert.Equal(2, read.Radio.RadioNumber); Assert.Equal(14_025_500, read.Radio.Frequency); Assert.Equal(14_030_000, read.Radio.TransmitFrequency); Assert.Equal("USB", read.Radio.Mode); Assert.True(read.Radio.IsSplit); } [Theory] [InlineData("not xml")] [InlineData("DL1ABC")] [InlineData("1")] public void Anything_but_a_radio_message_is_passed_over(string text) { Assert.Null(BroadcastMessages.ReadRadio(text)); } [Fact] public void A_deleted_spot_carries_no_comment_or_status() { SpotReport spot = new("DL1ABC", 7_012_300, "OK2XYZ", new DateTime(2026, 5, 30, 12, 0, 0), "CW", "up 1", "new qso"); XElement added = XElement.Parse(BroadcastMessages.Spot(spot, "add", "RUN-PC")); XElement deleted = XElement.Parse(BroadcastMessages.Spot(spot, "delete", "RUN-PC")); Assert.Equal("7012.3", added.Element("frequency")!.Value); Assert.Equal("new qso", added.Element("status")!.Value); Assert.Null(deleted.Element("status")); Assert.Null(deleted.Element("comment")); } private static ScoreReport Score(string state = "", string grid = "JO70", string assisted = "NON-ASSISTED") => new( "CQ-WW-CW", "OK1ABC ", "OK1ABC", "HIGH", assisted, "ONE", "SINGLE-OP-ASSISTED", "ALL", "CW", "", "", "", "OK", 15, 28, "", state, grid, ["ZN", "COUNTRYPREFIX"], [ new ScoreReportRow(3.5, "CW", 7, [2, 3], 18), new ScoreReportRow(ScoreLabels.TotalBand, "Both", 10, [3, 4], 25), ], 175, new DateTime(2026, 5, 30, 12, 0, 0)); [Fact] public void The_score_is_written_in_the_layout_N1MM_uses() { string xml = BroadcastMessages.Score(Score(state: "OK")); Assert.Equal( "\r\n\r\n" + "\tCQ-WW-CW\r\n" + "\tOK1ABC\r\n" + "\tOK1ABC\r\n" + "\t\r\n" + "\t\r\n" + "\t\r\n" + "\t\tOK\r\n" + "\t\t15\r\n" + "\t\t28\r\n" + "\t\tOK\r\n" + "\t\tJO70\r\n" + "\t\r\n" + "\t\r\n" + "\t\t7\r\n" + "\t\t2\r\n" + "\t\t3\r\n" + "\t\t18\r\n" + "\t\t10\r\n" + "\t\t3\r\n" + "\t\t4\r\n" + "\t\t25\r\n" + "\t\r\n" + "\t175\r\n" + "\t2026-05-30 12:00:00\r\n" + "\r\n", xml); } [Fact] public void The_score_leaves_out_an_empty_state_and_a_grid_that_is_not_four_or_six_letters() { string xml = BroadcastMessages.Score(Score(grid: "JO")); Assert.DoesNotContain("stprvoth", xml); Assert.DoesNotContain("grid", xml); } [Theory] [InlineData(1.8, "160")] [InlineData(3.5, "80")] [InlineData(5, "60")] [InlineData(7, "40")] [InlineData(10, "30")] [InlineData(14, "20")] [InlineData(18, "17")] [InlineData(21, "15")] [InlineData(24, "12")] [InlineData(28, "10")] [InlineData(50, "6")] [InlineData(70, "4")] [InlineData(144, "2")] [InlineData(222, "1.25")] [InlineData(420, "0.7")] [InlineData(902, "0.33")] [InlineData(1240, "0.23")] [InlineData(2300, "0.13")] [InlineData(3300, "0.09")] [InlineData(5650, "0.06")] [InlineData(10000, "0.03")] [InlineData(24000, "0.01")] [InlineData(47000, "0.0064")] [InlineData(76000, "0.004")] [InlineData(122250, "0.0025")] [InlineData(134000, "0.002")] [InlineData(241000, "0.0012")] [InlineData(0.1, "0")] [InlineData(0.5, "0")] [InlineData(ScoreLabels.TotalBand, "total")] public void A_band_is_named_by_its_wavelength_in_metres(double band, string label) { Assert.Equal(label, ScoreLabels.Band(band)); } [Theory] [InlineData("USB", "PH")] [InlineData("LSB", "PH")] [InlineData("SSB", "PH")] [InlineData("FM", "PH")] [InlineData("CW", "CW")] [InlineData("RCW", "CW")] [InlineData("RTTY", "RTTY")] [InlineData("RTTY-R", "RTTY")] [InlineData("FT8", "FT8")] [InlineData("PSK31", "PSK31")] [InlineData("JT65", "ALL")] [InlineData("Both", "ALL")] public void A_mode_is_folded_into_the_few_N1MM_names(string mode, string label) { Assert.Equal(label, ScoreLabels.Mode(mode)); } [Theory] [InlineData("CountryPrefix", "country")] [InlineData("Countryprefix", "country")] [InlineData("Sect", "state")] [InlineData("ZN", "zone")] [InlineData("WPXPrefix", "WPXPREFIX")] public void Three_multiplier_types_are_renamed_and_the_rest_are_upper_case(string name, string type) { Assert.Equal(type, ScoreLabels.MultiplierType(name)); } [Fact] public async Task A_kind_that_is_on_reaches_its_address_and_a_kind_that_is_off_does_not() { using UdpClient receiver = new(new IPEndPoint(IPAddress.Loopback, 0)); int port = ((IPEndPoint)receiver.Client.LocalEndPoint!).Port; using DataBroadcaster broadcaster = new(new Dictionary { [BroadcastKind.Radio] = new() { IsEnabled = true, Addresses = [$"127.0.0.1:{port}"] }, [BroadcastKind.Spots] = new() { Addresses = [$"127.0.0.1:{port}"] }, }); broadcaster.Send(BroadcastKind.Spots, ""); broadcaster.Send(BroadcastKind.Radio, ""); using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5)); UdpReceiveResult received = await receiver.ReceiveAsync(timeout.Token); Assert.Equal("", Encoding.UTF8.GetString(received.Buffer)); Assert.False(broadcaster.IsSending(BroadcastKind.Spots)); } [Fact] public void A_row_with_an_unreadable_address_is_left_off_and_reported() { using DataBroadcaster broadcaster = new(new Dictionary { [BroadcastKind.Score] = new() { IsEnabled = true, Addresses = ["nowhere"] }, }); Assert.False(broadcaster.IsSending(BroadcastKind.Score)); Assert.Single(broadcaster.Problems); } }