using Nonemm.Core; namespace Nonemm.Network.Tests; /// The computer-to-computer protocol on port 12070. It is N1MM's own, so the /// tests are about the bytes: a field in the wrong place is a contact with the /// callsign in the comment. public class StationRecordTests { [Fact] public void AMessageIsFramedTheWayN1MmFramesIt() { StationRecord record = new(7, "shack-pc", "IAM", ["7"]); Assert.Equal("DATA__07%SHACK-PC%IAM%7%~__DATA", record.ToWire()); } [Fact] public void AMessageReadsBackTheWayItWasWritten() { string wire = new StationRecord(3, "SHACK-PC", "TALK", ["hello", "there"]).ToWire(); StationRecord? read = StationRecord.Read(ref wire); Assert.NotNull(read); Assert.Equal(3, read.StationNumber); Assert.Equal("SHACK-PC", read.ComputerName); Assert.Equal("TALK", read.Type); Assert.Equal(["hello", "there"], read.Fields); } /// TCP hands over whatever has arrived, which is half a message as often as /// a whole one. [Fact] public void HalfAMessageIsHeldUntilTheRestArrives() { string whole = new StationRecord(1, "PC", "IAM", ["1"]).ToWire(); string text = whole[..10]; Assert.Null(StationRecord.Read(ref text)); text += whole[10..]; Assert.NotNull(StationRecord.Read(ref text)); } [Fact] public void TwoMessagesInOneReadAreBothFound() { string text = new StationRecord(1, "PC", "IAM", ["1"]).ToWire() + new StationRecord(2, "OTHER", "IAM", ["2"]).ToWire(); StationRecord? first = StationRecord.Read(ref text); StationRecord? second = StationRecord.Read(ref text); Assert.Equal("PC", first?.ComputerName); Assert.Equal("OTHER", second?.ComputerName); Assert.Null(StationRecord.Read(ref text)); } /// N1MM writes `!` in place of a delimiter that turns up in a field, so a /// comment with a per-cent sign in it does not split the message. [Fact] public void ADelimiterInsideAFieldIsReplaced() { StationRecord record = new(1, "PC", "TALK", ["100% sure~ok"]); string wire = record.ToWire(); StationRecord? read = StationRecord.Read(ref wire); Assert.Equal("100! sure!ok", read?.Field(0)); } [Fact] public void AFieldPastTheEndOfTheMessageIsEmpty() { StationRecord record = new(1, "PC", "IAM", ["1"]); Assert.Equal("", record.Field(9)); Assert.Equal(0, record.Number(9)); Assert.False(record.Flag(9)); } /// N1MM writes a boolean as Visual Basic prints one. Its own log holds -1 /// and 0 for the same thing, so both are read. [Theory] [InlineData("True", true)] [InlineData("False", false)] [InlineData("-1", true)] [InlineData("1", true)] [InlineData("0", false)] [InlineData("", false)] public void BooleansAreReadTheWayN1MmWritesThem(string field, bool expected) { StationRecord record = new(1, "PC", "XMIT", [field]); Assert.Equal(expected, record.Flag(0)); } [Fact] public void ABeaconCarriesTheSixFieldsN1MmCounts() { StationBeacon beacon = new("SHACK-PC", "192.168.1.5", 12070, "1.0.11364", "OM3KFF"); Assert.Equal("SHACK-PC%192.168.1.5%12070%1.0.11364%OM3KFF%%", beacon.ToWire()); Assert.Equal(beacon, StationBeacon.Read(beacon.ToWire())); } /// N1MM splits the beacon on `%` and refuses anything that is not seven /// long, which is how it turns away a station on an older version. [Fact] public void ABeaconWithTheWrongNumberOfFieldsIsNotABeacon() { Assert.Null(StationBeacon.Read("SHACK-PC%192.168.1.5%12070")); } /// The port-12060 contact broadcast pointed at the wrong port. N1MM says /// so out loud; this drops it. [Fact] public void XmlOnTheBeaconPortIsNotABeacon() { Assert.Null(StationBeacon.Read("")); } [Fact] public void ALoggedContactRoundTripsThroughTheWire() { Qso qso = Contact(); string wire = StationMessages.Logged(qso, 4, "SHACK-PC").ToWire(); StationRecord? record = StationRecord.Read(ref wire); ContactUpdate? update = StationMessages.Read(record!); ContactLogged logged = Assert.IsType(update); Assert.Equal("SHACK-PC", logged.StationName); Assert.Equal(qso.Call.Text, logged.Qso.Call.Text); Assert.Equal(qso.TimestampUtc, logged.Qso.TimestampUtc); Assert.Equal(qso.Frequency.Hertz, logged.Qso.Frequency.Hertz); Assert.Equal(qso.Mode.Name, logged.Qso.Mode.Name); Assert.Equal(qso.ContestName, logged.Qso.ContestName); Assert.Equal(qso.SentNumber, logged.Qso.SentNumber); Assert.Equal(qso.ReceivedNumber, logged.Qso.ReceivedNumber); Assert.Equal(qso.Zone, logged.Qso.Zone); Assert.Equal(qso.Points, logged.Qso.Points); Assert.True(logged.Qso.IsMultiplier1); Assert.False(logged.Qso.IsMultiplier2); Assert.Equal(qso.Operator, logged.Qso.Operator); Assert.Equal(qso.ContestNumber, logged.Qso.ContestNumber); Assert.Equal(qso.Continent, logged.Qso.Continent); Assert.Equal(4, logged.Qso.NetworkedComputerNumber); } /// A contact that arrived over the network was made somewhere else, which /// is what the log window colours a row on. [Fact] public void AContactFromAnotherStationIsNotOriginal() { string wire = StationMessages.Logged(Contact(), 4, "SHACK-PC").ToWire(); StationRecord? record = StationRecord.Read(ref wire); ContactLogged logged = Assert.IsType(StationMessages.Read(record!)); Assert.False(logged.Qso.IsOriginal); Assert.Equal("SHACK-PC", logged.Qso.StationName); } /// An edit carries the old callsign and the old time in front of the /// contact, because that pair is what N1MM keys a contact on. [Fact] public void AnEditSaysWhichRowToReplace() { Qso qso = Contact() with { Call = Callsign.Parse("DL1ABC") }; DateTime was = qso.TimestampUtc.AddMinutes(-3); string wire = StationMessages.Edited(qso, "DL1AB", was, 4, "SHACK-PC").ToWire(); StationRecord? record = StationRecord.Read(ref wire); ContactReplaced replaced = Assert.IsType(StationMessages.Read(record!)); Assert.Equal("DL1AB", replaced.OldCall); Assert.Equal(was, replaced.OldTimestampUtc); Assert.Equal("DL1ABC", replaced.Qso.Call.Text); } [Fact] public void ADeleteNamesTheContactAndTheContest() { Qso qso = Contact(); string wire = StationMessages.Deleted(qso, 4, "SHACK-PC").ToWire(); StationRecord? record = StationRecord.Read(ref wire); ContactDeleted deleted = Assert.IsType(StationMessages.Read(record!)); Assert.Equal(qso.Id, deleted.Id); Assert.Equal(qso.Call.Text, deleted.Call); Assert.Equal(qso.TimestampUtc, deleted.TimestampUtc); Assert.Equal(qso.ContestNumber, deleted.ContestNumber); } /// A resync is the same contact sent again, so it means the same thing. [Fact] public void AResyncedContactIsReadAsALoggedOne() { string wire = StationMessages.Resynced(Contact(), 4, "SHACK-PC").ToWire(); StationRecord? record = StationRecord.Read(ref wire); Assert.IsType(StationMessages.Read(record!)); } /// N1MM adds message types between versions. One this program does not /// know is passed over rather than treated as a fault. [Fact] public void AMessageTypeThisProgramDoesNotKnowIsPassedOver() { Assert.Null(StationMessages.Read(new StationRecord(1, "PC", "SKEDD", ["something"]))); } /// The frequency the other station transmits on is only kept when it is /// not the one it listens on: a contact in split is the exception, not the /// rule. [Fact] public void OneFrequencyForBothMeansNoSplit() { string wire = StationMessages.Logged(Contact(), 1, "PC").ToWire(); StationRecord? record = StationRecord.Read(ref wire); ContactLogged logged = Assert.IsType(StationMessages.Read(record!)); Assert.Equal(0, logged.Qso.QsxFrequency.Hertz); } [Fact] public void ASplitContactKeepsBothFrequencies() { Qso qso = Contact() with { QsxFrequency = Frequency.FromKilohertz(14_205) }; string wire = StationMessages.Logged(qso, 1, "PC").ToWire(); StationRecord? record = StationRecord.Read(ref wire); ContactLogged logged = Assert.IsType(StationMessages.Read(record!)); Assert.Equal(14_205_000, logged.Qso.QsxFrequency.Hertz); Assert.Equal(14_025_000, logged.Qso.Frequency.Hertz); } 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, SentReport = "599", ReceivedReport = "599", SentNumber = 41, ReceivedNumber = 17, Zone = 14, Points = 3, IsMultiplier1 = true, Operator = "OM3KFF", RadioNumber = 2, Continent = "EU", CountryPrefix = "G", StationPrefix = "G", WpxPrefix = "G3", Comment = "good sig", }; }