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",
};
}