diff --git a/JMayer.Example.WindowsService.sln b/JMayer.Example.WindowsService.sln
index 27d2a9d..4d39097 100644
--- a/JMayer.Example.WindowsService.sln
+++ b/JMayer.Example.WindowsService.sln
@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35208.52
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JMayer.Example.WindowsService", "JMayer.Example.WindowsService\JMayer.Example.WindowsService.csproj", "{0BC4DB3A-032F-43BA-BC39-380442F0BED0}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JMayer.Example.WindowsService", "JMayer.Example.WindowsService\JMayer.Example.WindowsService.csproj", "{0BC4DB3A-032F-43BA-BC39-380442F0BED0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "TestProject\TestProject.csproj", "{69EB99BC-2876-4339-AAFE-2AB2B406C0BA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,6 +17,10 @@ Global
{0BC4DB3A-032F-43BA-BC39-380442F0BED0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0BC4DB3A-032F-43BA-BC39-380442F0BED0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0BC4DB3A-032F-43BA-BC39-380442F0BED0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {69EB99BC-2876-4339-AAFE-2AB2B406C0BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {69EB99BC-2876-4339-AAFE-2AB2B406C0BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {69EB99BC-2876-4339-AAFE-2AB2B406C0BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {69EB99BC-2876-4339-AAFE-2AB2B406C0BA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/JMayer.Example.WindowsService/BSM/BMSEqualityComparer.cs b/JMayer.Example.WindowsService/BSM/BMSEqualityComparer.cs
new file mode 100644
index 0000000..64a6e81
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/BMSEqualityComparer.cs
@@ -0,0 +1,35 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class manages comparing two BSM objects.
+///
+public class BMSEqualityComparer : IEqualityComparer
+{
+ ///
+ public bool Equals(BSM? x, BSM? y)
+ {
+ if (x == null || y == null)
+ {
+ return false;
+ }
+
+ BaggageTagDetailEqualityComparer baggageTagDetailEqualityComparer = new();
+ OutboundFlightEqualityComparer outboundFlightEqualityComparer = new();
+ PassengerNameEqualityComparer passengerNameEqualityComparer = new();
+ VersionSupplementaryDataEqualityComparer versionSupplementaryDataEqualityComparer = new();
+
+ return baggageTagDetailEqualityComparer.Equals(x.BaggageTagDetails, y.BaggageTagDetails)
+ && x.ChangeOfStatus == y.ChangeOfStatus
+ && outboundFlightEqualityComparer.Equals(x.OutboundFlight, y.OutboundFlight)
+ && passengerNameEqualityComparer.Equals(x.PassengerName, y.PassengerName)
+ && versionSupplementaryDataEqualityComparer.Equals(x.VersionSupplementaryData, y.VersionSupplementaryData);
+ }
+
+ ///
+ public int GetHashCode([DisallowNull] BSM obj)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/BSM.cs b/JMayer.Example.WindowsService/BSM/BSM.cs
new file mode 100644
index 0000000..b7fa8b5
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/BSM.cs
@@ -0,0 +1,186 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class represents a simplified version of a baggage source message.
+///
+///
+/// A baggage source message contains the passenger information, outbound flight information,
+/// the baggage checked in at the ticket counter and other data. The outbound system uses this
+/// to bind a bag scanned at a scanner to a flight and to an end point in the system.
+///
+public class BSM : ITypeB
+{
+ ///
+ /// The constant for the Add change of status.
+ ///
+ public const string Add = "ADD";
+
+ ///
+ /// The property gets the baggage tag details.
+ ///
+ public BaggageTagDetails? BaggageTagDetails { get; set; }
+
+ ///
+ /// The constant for the change change of status.
+ ///
+ public const string Change = "CHG";
+
+ ///
+ /// The property gets the change of status.
+ ///
+ [Required]
+ public string ChangeOfStatus { get; set; } = string.Empty;
+
+ ///
+ /// The constant for the delete change of status.
+ ///
+ public const string Delete = "DEL";
+
+ ///
+ /// The constant for the end of BSM.
+ ///
+ public const string EndOfBSM = "ENDBSM";
+
+ ///
+ /// The property gets the outbound flight information.
+ ///
+ public OutboundFlight? OutboundFlight { get; set; }
+
+ ///
+ /// The property gets the passenger name.
+ ///
+ public PassengerName? PassengerName { get; set; }
+
+ ///
+ /// The property gets when the BSM was received.
+ ///
+ public DateTime ReceivedOn { get; init; } = DateTime.Now;
+
+ ///
+ /// The constant for the start of BSM.
+ ///
+ public const string StartOfBSM = "BSM";
+
+ ///
+ /// The property gets the version supplementary data.
+ ///
+ public VersionSupplementaryData? VersionSupplementaryData { get; set; }
+
+ ///
+ /// The method returns the change of status from the BSM.
+ ///
+ /// The BSM to examine.
+ /// The change of status.
+ private static string GetChangeOfStatus(string bsm)
+ {
+ string changeOfStatus = bsm.Substring(0, 3);
+
+ if (changeOfStatus == Change)
+ {
+ return Change;
+ }
+ else if (changeOfStatus == Delete)
+ {
+ return Delete;
+ }
+ else
+ {
+ return Add;
+ }
+ }
+
+ ///
+ public void Parse(string typeBString)
+ {
+ //Remove the end identifier because it's no longer needed.
+ //This needs to be removed before start else you end up with only END.
+ typeBString = typeBString.Replace($"{EndOfBSM}{Environment.NewLine}", string.Empty);
+ typeBString = typeBString.Replace(EndOfBSM, string.Empty);
+
+ //Remove the start identifier because it's no longer needed.
+ typeBString = typeBString.Replace($"{StartOfBSM}{Environment.NewLine}", string.Empty);
+ typeBString = typeBString.Replace(StartOfBSM, string.Empty);
+
+ //Get the change of status and then remove it because its no longer needed
+ ChangeOfStatus = GetChangeOfStatus(typeBString);
+ typeBString = typeBString.Replace($"{ChangeOfStatus}{Environment.NewLine}", string.Empty);
+ typeBString = typeBString.Replace(ChangeOfStatus, string.Empty);
+
+ int totalBytesProcessed = 0;
+
+ do
+ {
+ int startIndex = typeBString.IndexOf('.', totalBytesProcessed);
+
+ //Dot was not found so exit.
+ if (startIndex == -1)
+ {
+ break;
+ }
+
+ int endIndex = typeBString.IndexOf('.', startIndex + 1);
+
+ //If the next dot is not found then assume this is the last line.
+ if (endIndex == -1)
+ {
+ endIndex = typeBString.Length;
+ }
+
+ string line = typeBString.Substring(startIndex, endIndex - startIndex);
+
+ if (line.StartsWith(OutboundFlight.DotFElement))
+ {
+ OutboundFlight = new OutboundFlight();
+ OutboundFlight.Parse(line);
+ }
+ else if (line.StartsWith(BaggageTagDetails.DotNElement))
+ {
+ BaggageTagDetails = new BaggageTagDetails();
+ BaggageTagDetails.Parse(line);
+ }
+ else if (line.StartsWith(PassengerName.DotPElement))
+ {
+ PassengerName = new PassengerName();
+ PassengerName.Parse(line);
+ }
+ else if (line.StartsWith(VersionSupplementaryData.DotVElement))
+ {
+ VersionSupplementaryData = new VersionSupplementaryData();
+ VersionSupplementaryData.Parse(line);
+ }
+
+ totalBytesProcessed += line.Length;
+
+ } while (totalBytesProcessed < typeBString.Length);
+ }
+
+ ///
+ public string ToTypeB()
+ {
+ string dotElements = string.Empty;
+
+ if (OutboundFlight != null)
+ {
+ dotElements += OutboundFlight.ToTypeB();
+ }
+
+ if (BaggageTagDetails != null)
+ {
+ dotElements += BaggageTagDetails.ToTypeB();
+ }
+
+ if (PassengerName != null)
+ {
+ dotElements += PassengerName.ToTypeB();
+ }
+
+ if (VersionSupplementaryData != null)
+ {
+ dotElements += VersionSupplementaryData.ToTypeB();
+ }
+
+ return $"{StartOfBSM}{Environment.NewLine}{ChangeOfStatus}{Environment.NewLine}{dotElements}{EndOfBSM}{Environment.NewLine}";
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/BSMGenerator.cs b/JMayer.Example.WindowsService/BSM/BSMGenerator.cs
new file mode 100644
index 0000000..fd128b4
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/BSMGenerator.cs
@@ -0,0 +1,270 @@
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class manages generating a BSM.
+///
+public class BSMGenerator
+{
+ ///
+ /// The class of travels.
+ ///
+ private readonly string[] _classOfTravels =
+ [
+ "A",
+ "B",
+ "C",
+ "D",
+ "E",
+ "F",
+ "G",
+ "H",
+ "I",
+ "J",
+ "K",
+ "L",
+ "M",
+ "N",
+ "O",
+ "P",
+ "Q",
+ "R",
+ "S",
+ "T",
+ "U",
+ "V",
+ "W",
+ "X",
+ "Y",
+ "Z"
+ ];
+
+ ///
+ /// The destinations.
+ ///
+ private readonly string[] _destinations =
+ [
+ "CAE",
+ "BNA",
+ "LBB",
+ "MSY",
+ "OAK",
+ "PIE",
+ "RSW",
+ "VPS",
+ ];
+
+ ///
+ /// The destination being used.
+ ///
+ private int _destinationIndex = 0;
+
+ ///
+ /// The flight number for the BSM.
+ ///
+ private int _flightNumber = MinFlightNumber;
+
+ ///
+ /// Used to generate IATAs.
+ ///
+ private readonly IATAGenerator[] _iataGenerators =
+ [
+ new IATAGenerator() { AirlineAlphaNumericCode = AmericanAirlineAlphaCode, AirlineNumericCode = AmericanAirlinesNumericCode, },
+ new IATAGenerator() { AirlineAlphaNumericCode = DeltaAlphaCode, AirlineNumericCode = DeltaNumericCode, },
+ new IATAGenerator() { AirlineAlphaNumericCode = UnitedAirlinesAlphaCode, AirlineNumericCode = UnitedAirlinesNumericCode, },
+ new IATAGenerator() { AirlineAlphaNumericCode = SouthwestAlpaCode, AirlineNumericCode = SouthwestNumericCode, },
+ ];
+
+ ///
+ /// The IATA generator being used.
+ ///
+ private int _iataGeneratorIndex = 0;
+
+ ///
+ /// The passenger count.
+ ///
+ private int _passengerCount = MinPassengerCount;
+
+ ///
+ /// The constant for the AA alphanumeric code.
+ ///
+ public const string AmericanAirlineAlphaCode = "AA";
+
+ ///
+ /// The constant for the AA numeric code.
+ ///
+ public const string AmericanAirlinesNumericCode = "001";
+
+ ///
+ /// The constant for the DL alphanumeric code.
+ ///
+ public const string DeltaAlphaCode = "DL";
+
+ ///
+ /// The constant for the DL numeric code.
+ ///
+ public const string DeltaNumericCode = "006";
+
+ ///
+ /// The constant for the .P given name.
+ ///
+ public const string DotPGivenName = "PASSENGER";
+
+ ///
+ /// The constant for the .P surname.
+ ///
+ public const string DotPSurName = "TEST";
+
+ ///
+ /// The constant for the .V airport code.
+ ///
+ public const string DotVAirportCode = "MCO";
+
+ ///
+ /// The constant for the .V data dictionary version number.
+ ///
+ public const int DotVDataDictionaryVersionNumber = 1;
+
+ ///
+ /// The constant for the maximum flight number.
+ ///
+ public const int MaxFlightNumber = 9999;
+
+ ///
+ /// The constant for the minimum flight number.
+ ///
+ public const int MinFlightNumber = 1;
+
+ ///
+ /// The constant for the maximum number of passengers.
+ ///
+ public const int MaxPassengerCount = 100;
+
+ ///
+ /// The constant for the minimum number of passengers.
+ ///
+ public const int MinPassengerCount = 1;
+
+ ///
+ /// The constant for the WN alphanumeric code.
+ ///
+ public const string SouthwestAlpaCode = "WN";
+
+ ///
+ /// The constant for the WN numeric code.
+ ///
+ public const string SouthwestNumericCode = "526";
+
+ ///
+ /// The constant for the UA alphanumeric code.
+ ///
+ public const string UnitedAirlinesAlphaCode = "UA";
+
+ ///
+ /// The constant for the UA numeric code.
+ ///
+ public const string UnitedAirlinesNumericCode = "016";
+
+ ///
+ /// The default constructor.
+ ///
+ public BSMGenerator()
+ {
+ SetRandomDestination();
+ }
+
+ ///
+ /// The method returns the next BSM.
+ ///
+ /// The BSM.
+ public BSM Generate()
+ {
+ IATAGenerator generator = _iataGenerators[_iataGeneratorIndex];
+
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [generator.Generate()],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = generator.AirlineAlphaNumericCode,
+ ClassOfTravel = _classOfTravels[new Random(DateTime.Now.Second).Next(0, _classOfTravels.Length - 1)],
+ Destination = _destinations[_destinationIndex],
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = _flightNumber.ToString().PadLeft(4, '0'),
+ },
+ PassengerName = new()
+ {
+ GivenNames = [$"{DotPGivenName}{_passengerCount}"],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ IncrementPassengerCount();
+
+ return bsm;
+ }
+
+ ///
+ /// The method increments the flight number.
+ ///
+ private void IncrementFlightNumber()
+ {
+ _flightNumber++;
+
+ if (_flightNumber > MaxFlightNumber)
+ {
+ _flightNumber = MinFlightNumber;
+ }
+ }
+
+ ///
+ /// The method increments to the next IATA generator.
+ ///
+ private void IncrementIATAGenerator()
+ {
+ _iataGeneratorIndex++;
+
+ if (_iataGeneratorIndex == _iataGenerators.Length)
+ {
+ _iataGeneratorIndex = 0;
+ }
+ }
+
+ ///
+ /// The method increments the passenger count.
+ ///
+ ///
+ /// When the maximum number of passengers are reached, the flight number is incremented,
+ /// a new IATA generator is used and a new destination is randomly chosen. The idea is the
+ /// generator will generate X BSMs for a flight and then a new one is used.
+ ///
+ private void IncrementPassengerCount()
+ {
+ _passengerCount++;
+
+ if (_passengerCount > MaxPassengerCount)
+ {
+ _passengerCount = MinPassengerCount;
+ IncrementFlightNumber();
+ IncrementIATAGenerator();
+ SetRandomDestination();
+ }
+ }
+
+ ///
+ /// The method sets a random destination.
+ ///
+ private void SetRandomDestination()
+ {
+ _destinationIndex = new Random(DateTime.Now.Second).Next(0, _destinations.Length - 1);
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/BSMPDU.cs b/JMayer.Example.WindowsService/BSM/BSMPDU.cs
new file mode 100644
index 0000000..9fd1fcc
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/BSMPDU.cs
@@ -0,0 +1,50 @@
+using JMayer.Net.ProtocolDataUnit;
+using System.ComponentModel.DataAnnotations;
+using System.Text;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class represents a BSM PDU.
+///
+public class BSMPDU : PDU
+{
+ ///
+ /// The property gets/sets the BSM string.
+ ///
+ public BSM BSM { get; init; } = new();
+
+ ///
+ public override byte[] ToBytes()
+ {
+ return Encoding.ASCII.GetBytes(BSM.ToTypeB());
+ }
+
+ ///
+ public override List Validate()
+ {
+ List validationResults = [];
+
+ if (BSM.BaggageTagDetails != null)
+ {
+ Validator.TryValidateObject(BSM.BaggageTagDetails, new ValidationContext(BSM.BaggageTagDetails), validationResults, validateAllProperties: true);
+ }
+
+ if (BSM.OutboundFlight != null)
+ {
+ Validator.TryValidateObject(BSM.OutboundFlight, new ValidationContext(BSM.OutboundFlight), validationResults, validateAllProperties: true);
+ }
+
+ if (BSM.PassengerName != null)
+ {
+ Validator.TryValidateObject(BSM.PassengerName, new ValidationContext(BSM.PassengerName), validationResults, validateAllProperties: true);
+ }
+
+ if (BSM.VersionSupplementaryData != null)
+ {
+ Validator.TryValidateObject(BSM.VersionSupplementaryData, new ValidationContext(BSM.VersionSupplementaryData), validationResults, validateAllProperties: true);
+ }
+
+ return validationResults;
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/BSMParser.cs b/JMayer.Example.WindowsService/BSM/BSMParser.cs
new file mode 100644
index 0000000..a34597c
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/BSMParser.cs
@@ -0,0 +1,62 @@
+using JMayer.Net.ProtocolDataUnit;
+using System.Text;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class manages parsing the BSM.
+///
+public class BSMParser : PDUParser
+{
+ ///
+ protected override PDUParserResult SubClassParse(byte[] bytes)
+ {
+ int totalBytesProcessed = 0;
+ List pdus = [];
+
+ string bytesAsString = Encoding.ASCII.GetString(bytes);
+
+ do
+ {
+ int startIndex = bytesAsString.IndexOf(BSM.StartOfBSM, totalBytesProcessed);
+
+ //Start was not found so exit.
+ if (startIndex == -1)
+ {
+ break;
+ }
+
+ int endIndex = bytesAsString.IndexOf(BSM.EndOfBSM, startIndex);
+
+ //End was not found or start is actually the end, exit.
+ if (endIndex == -1 || startIndex == endIndex)
+ {
+ break;
+ }
+
+ //Add the length of the end of BSM so the end is included.
+ endIndex += BSM.EndOfBSM.Length;
+
+ //Ensures the new line is included, if it exists.
+ if (endIndex < bytesAsString.Length && bytesAsString[endIndex] == '\n')
+ {
+ endIndex++;
+ }
+ else if (endIndex + 1 < bytesAsString.Length && bytesAsString[endIndex] == '\r' && bytesAsString[endIndex + 1] == '\n')
+ {
+ endIndex += 2;
+ }
+
+ string bsmString = bytesAsString.Substring(startIndex, endIndex - startIndex);
+
+ BSMPDU pdu = new();
+ pdu.BSM.Parse(bsmString);
+
+ pdus.Add(pdu);
+ totalBytesProcessed += endIndex - startIndex;
+
+ } while (totalBytesProcessed < bytes.Length);
+
+ return new PDUParserResult(pdus, totalBytesProcessed);
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/BaggageTagDetailEqualityComparer.cs b/JMayer.Example.WindowsService/BSM/BaggageTagDetailEqualityComparer.cs
new file mode 100644
index 0000000..59e5741
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/BaggageTagDetailEqualityComparer.cs
@@ -0,0 +1,47 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class manages comparing two BaggageTagDetail objects.
+///
+public class BaggageTagDetailEqualityComparer : IEqualityComparer
+{
+ ///
+ public bool Equals(BaggageTagDetails? x, BaggageTagDetails? y)
+ {
+ if (x == null && y == null)
+ {
+ return true;
+ }
+ else if (x != null && y != null)
+ {
+ if (x.Count != y.Count)
+ {
+ return false;
+ }
+ else
+ {
+ foreach (string tag in x.BaggageTagNumbers)
+ {
+ if (!y.BaggageTagNumbers.Contains(tag))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ public int GetHashCode([DisallowNull] BaggageTagDetails obj)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/BaggageTagDetails.cs b/JMayer.Example.WindowsService/BSM/BaggageTagDetails.cs
new file mode 100644
index 0000000..52492dc
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/BaggageTagDetails.cs
@@ -0,0 +1,65 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class represents the baggage tag details in the BSM.
+///
+public class BaggageTagDetails : ITypeB
+{
+ ///
+ /// The property gets the number of baggage tag numbers.
+ ///
+ public int Count
+ {
+ get => BaggageTagNumbers.Count;
+ }
+
+ ///
+ /// The property gets a list of baggage tag numbers.
+ ///
+ ///
+ /// Index 0 must be the first tag and index N must be the last tag. Each tag
+ /// after the first must increment by 1.
+ ///
+ [Length(1, 999)]
+ public List BaggageTagNumbers { get; init; } = [];
+
+ ///
+ /// The constant for the .N element.
+ ///
+ public const string DotNElement = ".N";
+
+ ///
+ public void Parse(string typeBString)
+ {
+ //Remove the identifier and new line.
+ typeBString = typeBString.Replace($"{DotNElement}/", string.Empty);
+ typeBString = typeBString.Replace(Environment.NewLine, string.Empty);
+
+ if (typeBString.Length is 13)
+ {
+ if (long.TryParse(typeBString.AsSpan(0, 10), out long iataNumber) && int.TryParse(typeBString.AsSpan(10, 3), out int length))
+ {
+ for (int index = 0; index < length; index++)
+ {
+ string iataString = (iataNumber + index).ToString().PadLeft(10, '0');
+ BaggageTagNumbers.Add(iataString);
+ }
+ }
+ }
+ }
+
+ ///
+ public string ToTypeB()
+ {
+ if (Count == 0)
+ {
+ return string.Empty;
+ }
+ else
+ {
+ return $"{DotNElement}/{BaggageTagNumbers[0]}{Count:D3}{Environment.NewLine}";
+ }
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/DateTimeExtensions.cs b/JMayer.Example.WindowsService/BSM/DateTimeExtensions.cs
new file mode 100644
index 0000000..60beb33
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/DateTimeExtensions.cs
@@ -0,0 +1,15 @@
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The statis class contains extension methods for the DateTime.
+///
+public static class DateTimeExtensions
+{
+ ///
+ /// The method returns the date time as a string in the 01JAN format.
+ ///
+ /// The date time to be formatted.
+ /// A formatted string.
+ public static string ToDayMonthFormat(this DateTime dateTime)
+ => $"{dateTime.Day:00}{dateTime.ToString("MMMM").Substring(0, 3).ToUpper()}";
+}
diff --git a/JMayer.Example.WindowsService/BSM/IATAGenerator.cs b/JMayer.Example.WindowsService/BSM/IATAGenerator.cs
new file mode 100644
index 0000000..8f82873
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/IATAGenerator.cs
@@ -0,0 +1,57 @@
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class manages generating an IATA (10 digit tag number).
+///
+public class IATAGenerator
+{
+ ///
+ /// Keeps track of the sequence number to be used.
+ ///
+ private int _sequenceNumber = MinSequenceNumber;
+
+ ///
+ /// The property gets/sets the alphanumeric airline code.
+ ///
+ ///
+ /// Used by the BSM generator.
+ ///
+ public string AirlineAlphaNumericCode { get; init; } = string.Empty;
+
+ ///
+ /// The property gets/sets the numeric airline code used by the IATA.
+ ///
+ public string AirlineNumericCode { get; init; } = string.Empty;
+
+ ///
+ /// The constant for the maximum sequence number.
+ ///
+ public const int MaxSequenceNumber = 999999;
+
+ ///
+ /// The constant for the minimum sequence number.
+ ///
+ public const int MinSequenceNumber = 1;
+
+ ///
+ /// The method returns the next IATA.
+ ///
+ /// The IATA.
+ ///
+ /// The IATA format is the first digit is 0 or 2-9, the next 3 digits are the airline numeric code
+ /// and the last 6 digits is a sequence number from 1-999999.
+ ///
+ public string Generate()
+ {
+ string iata = $"0{AirlineNumericCode}{_sequenceNumber.ToString().PadLeft(6, '0')}";
+
+ _sequenceNumber++;
+
+ if (_sequenceNumber > MaxSequenceNumber)
+ {
+ _sequenceNumber = MinSequenceNumber;
+ }
+
+ return iata;
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/ITypeB.cs b/JMayer.Example.WindowsService/BSM/ITypeB.cs
new file mode 100644
index 0000000..aa235b9
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/ITypeB.cs
@@ -0,0 +1,19 @@
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The interface contains common methods for the type B string format.
+///
+public interface ITypeB
+{
+ ///
+ /// The method parses a type B string.
+ ///
+ /// The type B string to parse.
+ void Parse(string typeBString);
+
+ ///
+ /// The method returns the object in the type B string format.
+ ///
+ /// A string in the type B format.
+ string ToTypeB();
+}
diff --git a/JMayer.Example.WindowsService/BSM/OutboundFlight.cs b/JMayer.Example.WindowsService/BSM/OutboundFlight.cs
new file mode 100644
index 0000000..ecab295
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/OutboundFlight.cs
@@ -0,0 +1,107 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class represents the outbound flight information in the BSM.
+///
+public class OutboundFlight : ITypeB
+{
+ ///
+ /// The property gets the airline for the flight.
+ ///
+ [Required]
+ [RegularExpression("^[A-Z0-9]{2}$", ErrorMessage = "The airline must be 2 alphanumeric characters; the letters must be capital.")]
+ public string Airline { get; set; } = string.Empty;
+
+ ///
+ /// The property gets the class of travel for passenger for this flight.
+ ///
+ [RegularExpression("^([A-Z]{1})?$", ErrorMessage = "The class of travel must be 1 capital letter or empty.")]
+ public string ClassOfTravel { get; set; } = string.Empty;
+
+ ///
+ /// The property gets the destination for this flight.
+ ///
+ [RegularExpression("^([A-Z]{3})?$", ErrorMessage = "The destination must be 3 capital letters or empty.")]
+ public string Destination { get; set; } = string.Empty;
+
+ ///
+ /// The constant for the .F element.
+ ///
+ public const string DotFElement = ".F";
+
+ ///
+ /// The property gets the date this flight flies.
+ ///
+ ///
+ /// This will be formatted like 01JAN.
+ ///
+ [Required]
+ [RegularExpression("^[0-9]{2}(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)$", ErrorMessage = "The flight date must be the day of the month (2 digits) followed by the first three capital letters of the month.")]
+ public string FlightDate { get; set; } = string.Empty;
+
+ ///
+ /// The property gets the identifier for the flight.
+ ///
+ [Required]
+ [RegularExpression("^([0-9]{4})|([0-9]{4}[A-Z]{1})$", ErrorMessage = "The flight number must be 4 digits and optionally a capital letter.")]
+ public string FlightNumber { get; set; } = string.Empty;
+
+ ///
+ public void Parse(string typeBString)
+ {
+ //Remove the identifier so the elements can be broken apart with Split().
+ typeBString = typeBString.Replace($"{DotFElement}/", string.Empty);
+ typeBString = typeBString.Replace(Environment.NewLine, string.Empty);
+
+ string[] elements = typeBString.Split('/');
+
+ //Handle parsing the airline and flight number.
+ if (elements.Length > 0 && !string.IsNullOrEmpty(elements[0]))
+ {
+ string airlineAndFlight = elements[0];
+
+ if (airlineAndFlight.Length >= 6)
+ {
+ Airline = airlineAndFlight.Substring(0, 2);
+ FlightNumber = airlineAndFlight.Substring(2, airlineAndFlight.Length - 2);
+ }
+ }
+
+ //Handle parsing the flight date.
+ if (elements.Length > 1 && !string.IsNullOrEmpty(elements[1]))
+ {
+ FlightDate = elements[1];
+ }
+
+ //Handle parsing the destination.
+ if (elements.Length > 2 && !string.IsNullOrEmpty(elements[2]))
+ {
+ Destination = elements[2];
+ }
+
+ //Handle parsing the class of travel.
+ if (elements.Length > 3 && !string.IsNullOrEmpty(elements[3]))
+ {
+ ClassOfTravel = elements[3];
+ }
+ }
+
+ ///
+ public string ToTypeB()
+ {
+ if (!string.IsNullOrEmpty(ClassOfTravel))
+ {
+ return $"{DotFElement}/{Airline}{FlightNumber}/{FlightDate}/{Destination}/{ClassOfTravel}{Environment.NewLine}";
+ }
+ else if (!string.IsNullOrEmpty(Destination))
+ {
+ return $"{DotFElement}/{Airline}{FlightNumber}/{FlightDate}/{Destination}{Environment.NewLine}";
+ }
+ else
+ {
+ return $"{DotFElement}/{Airline}{FlightNumber}/{FlightDate}{Environment.NewLine}";
+ }
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/OutboundFlightEqualityComparer.cs b/JMayer.Example.WindowsService/BSM/OutboundFlightEqualityComparer.cs
new file mode 100644
index 0000000..0715ea0
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/OutboundFlightEqualityComparer.cs
@@ -0,0 +1,36 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class manages comparing two OutboundFlight objects.
+///
+public class OutboundFlightEqualityComparer : IEqualityComparer
+{
+ ///
+ public bool Equals(OutboundFlight? x, OutboundFlight? y)
+ {
+ if (x == null && y == null)
+ {
+ return true;
+ }
+ else if (x != null && y != null)
+ {
+ return x.Airline == y.Airline
+ && x.ClassOfTravel == y.ClassOfTravel
+ && x.Destination == y.Destination
+ && x.FlightDate == y.FlightDate
+ && x.FlightNumber == y.FlightNumber;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ public int GetHashCode([DisallowNull] OutboundFlight obj)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/PassengerName.cs b/JMayer.Example.WindowsService/BSM/PassengerName.cs
new file mode 100644
index 0000000..7b51b5b
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/PassengerName.cs
@@ -0,0 +1,80 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.RegularExpressions;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class represents the passenger name in the BSM.
+///
+public class PassengerName : ITypeB
+{
+ ///
+ /// The constant for the .P element.
+ ///
+ public const string DotPElement = ".P";
+
+ ///
+ /// The property gets/sets the given names for the passenger.
+ ///
+ public List GivenNames { get; init; } = [];
+
+ ///
+ /// The property gets/sets the surnaame for the passenger.
+ ///
+ [Required]
+ public string SurName { get; set; } = string.Empty;
+
+ ///
+ public void Parse(string typeBString)
+ {
+ //Remove the identifier so the elements can be broken apart with Split().
+ typeBString = typeBString.Replace($"{DotPElement}/", string.Empty);
+ typeBString = typeBString.Replace(Environment.NewLine , string.Empty);
+
+ string[] elements = typeBString.Split('/');
+
+ //Handle parsing the surname.
+ if (elements.Length > 0 && !string.IsNullOrEmpty(elements[0]))
+ {
+ //The number of given names can be infront of the surname as either a 1 or 2 digit number
+ //so remove the number if it exists.
+ if (elements[0].Length >= 2 && Regex.IsMatch(elements[0].AsSpan(0, 2), "^\\d$"))
+ {
+ SurName = elements[0].Substring(2);
+ }
+ else if (elements[0].Length >= 1 && Regex.IsMatch(elements[0].AsSpan(0, 1), "^\\d$"))
+ {
+ SurName = elements[0].Substring(1);
+ }
+ else
+ {
+ SurName = elements[0];
+ }
+ }
+
+ //The rest of the elements will be the given names so add them to the list.
+ if (elements.Length > 1)
+ {
+ for (int index = 1; index < elements.Length; index++)
+ {
+ if (!string.IsNullOrEmpty(elements[index]))
+ {
+ GivenNames.Add(elements[index]);
+ }
+ }
+ }
+ }
+
+ ///
+ public string ToTypeB()
+ {
+ string givenNames = string.Empty;
+
+ foreach (string givenName in GivenNames)
+ {
+ givenNames += $"/{givenName}";
+ }
+
+ return $"{DotPElement}/{GivenNames.Count}{SurName}{givenNames}{Environment.NewLine}";
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/PassengerNameEqualityComparer.cs b/JMayer.Example.WindowsService/BSM/PassengerNameEqualityComparer.cs
new file mode 100644
index 0000000..23a59db
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/PassengerNameEqualityComparer.cs
@@ -0,0 +1,47 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class manages comparing two PassengerName objects.
+///
+public class PassengerNameEqualityComparer : IEqualityComparer
+{
+ ///
+ public bool Equals(PassengerName? x, PassengerName? y)
+ {
+ if (x == null && y == null)
+ {
+ return true;
+ }
+ else if (x != null && y != null)
+ {
+ if (x.SurName != y.SurName || x.GivenNames.Count != y.GivenNames.Count)
+ {
+ return false;
+ }
+ else
+ {
+ foreach (string givenName in x.GivenNames)
+ {
+ if (!y.GivenNames.Contains(givenName))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ public int GetHashCode([DisallowNull] PassengerName obj)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/VersionSupplementaryData.cs b/JMayer.Example.WindowsService/BSM/VersionSupplementaryData.cs
new file mode 100644
index 0000000..f1893b4
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/VersionSupplementaryData.cs
@@ -0,0 +1,82 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class represents the version & supplementary data in the BSM.
+///
+public class VersionSupplementaryData : ITypeB
+{
+ ///
+ /// The property gets the aiport who sent the BSM.
+ ///
+ [Required]
+ [RegularExpression("^[A-Z]{3}$", ErrorMessage = "The airport code must be 3 capital letters.")]
+ public string AirportCode { get; set; } = string.Empty;
+
+ ///
+ /// The property gets the baggage source indicator (local, transfer, remote or terminating).
+ ///
+ [Required]
+ [RegularExpression("^(L|R|X|T)$", ErrorMessage = "The baggage source indicator must be L, R, X or T.")]
+ public string BaggageSourceIndicator { get; set; } = string.Empty;
+
+ ///
+ /// The property gets the version number for the data dictionary.
+ ///
+ [Required]
+ [Range(1, 9)]
+ public int DataDictionaryVersionNumber { get; set; }
+
+ ///
+ /// The constant for the .V element.
+ ///
+ public const string DotVElement = ".V";
+
+ ///
+ /// The constant for the local baggage source indicator.
+ ///
+ public const string LocalBaggageSourceIndicator = "L";
+
+ ///
+ /// The constant for the remote baggage source indicator.
+ ///
+ public const string RemoteBaggageSourceIndicator = "R";
+
+ ///
+ /// The constant for the terminating baggage source indicator.
+ ///
+ public const string TerminatingBaggageSourceIndicator = "X";
+
+ ///
+ /// The constant for the transfer baggage source indicator.
+ ///
+ public const string TransferBaggageSourceIndicator = "T";
+
+ ///
+ public void Parse(string typeBString)
+ {
+ //Remove the identifier so the elements can be broken apart with Split().
+ typeBString = typeBString.Replace($"{DotVElement}/", string.Empty);
+ typeBString = typeBString.Replace(Environment.NewLine, string.Empty);
+
+ string[] elements = typeBString.Split('/');
+
+ if (elements.Length > 0 && elements[0].Length is 5)
+ {
+ if (int.TryParse(elements[0].AsSpan(0, 1), out int dataDictionaryVersionNumber))
+ {
+ DataDictionaryVersionNumber = dataDictionaryVersionNumber;
+ }
+
+ BaggageSourceIndicator = elements[0].Substring(1, 1);
+ AirportCode = elements[0].Substring(2, 3);
+ }
+ }
+
+ ///
+ public string ToTypeB()
+ {
+ return $"{DotVElement}/{DataDictionaryVersionNumber}{BaggageSourceIndicator}{AirportCode}{Environment.NewLine}";
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSM/VersionSupplementaryDataEqualityComparer.cs b/JMayer.Example.WindowsService/BSM/VersionSupplementaryDataEqualityComparer.cs
new file mode 100644
index 0000000..0f6c3d5
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSM/VersionSupplementaryDataEqualityComparer.cs
@@ -0,0 +1,34 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace JMayer.Example.WindowsService.BSM;
+
+///
+/// The class manages comparing two VersionSupplementaryData objects.
+///
+public class VersionSupplementaryDataEqualityComparer : IEqualityComparer
+{
+ ///
+ public bool Equals(VersionSupplementaryData? x, VersionSupplementaryData? y)
+ {
+ if (x == null && y == null)
+ {
+ return true;
+ }
+ else if (x != null && y != null)
+ {
+ return x.AirportCode == y.AirportCode
+ && x.BaggageSourceIndicator == y.BaggageSourceIndicator
+ && x.DataDictionaryVersionNumber == y.DataDictionaryVersionNumber;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ public int GetHashCode([DisallowNull] VersionSupplementaryData obj)
+ {
+ return obj.GetHashCode();
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSMClientWorker.cs b/JMayer.Example.WindowsService/BSMClientWorker.cs
new file mode 100644
index 0000000..a670454
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSMClientWorker.cs
@@ -0,0 +1,93 @@
+using JMayer.Example.WindowsService.BSM;
+using JMayer.Net;
+using JMayer.Net.ProtocolDataUnit;
+
+namespace JMayer.Example.WindowsService;
+
+///
+/// The class manages connecting to the server and processes BSMs received from the server.
+///
+internal class BSMClientWorker : BackgroundService
+{
+ ///
+ /// Used to log activity for the service.
+ ///
+ private readonly ILogger _logger;
+
+ ///
+ /// Used to manage TCP/IP communication.
+ ///
+ private readonly IClient _client;
+
+ ///
+ /// The dependency injection constructor.
+ ///
+ /// Used to log activity for the service.
+ /// Used to manage TCP/IP communication.
+ public BSMClientWorker(ILogger logger, IClient client)
+ {
+ _logger = logger;
+ _client = client;
+ }
+
+ ///
+ /// The method manages connecting to the server and processes BSMs received from the server.
+ ///
+ /// Used to cancel the task when the service stops.
+ /// A Task object for the async.
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ if (!_client.IsConnected)
+ {
+ try
+ {
+ await _client.ConnectAsync("127.0.0.1", BSMServerConnectionWorker.Port, stoppingToken);
+ _logger.LogInformation("The client connected to the BSM server.");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "The client failed to connect to the BSM server.");
+ }
+ }
+ else
+ {
+ List pdus = await _client.ReceiveAndParseAsync(stoppingToken);
+
+ foreach (BSMPDU pdu in pdus.Cast())
+ {
+ if (pdu.IsValid)
+ {
+ _logger.LogInformation("The client received a valid BSM from the server. {BSM}", pdu.BSM.ToTypeB());
+ }
+ else
+ {
+ _logger.LogWarning("The client received an invalid BSM from the server. {BSM}", pdu.BSM.ToTypeB());
+ }
+ }
+ }
+
+ await Task.Delay(1000);
+ }
+ }
+
+ ///
+ ///
+ /// This is overriden so the client can disconnect from the server.
+ ///
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ _client.Disconnect();
+ _logger.LogInformation("The client disconnect from the server.");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "The client failed to disconnect from the server; it may have already been disconnected by the server.");
+ }
+
+ await base.StopAsync(cancellationToken);
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSMServerConnectionWorker.cs b/JMayer.Example.WindowsService/BSMServerConnectionWorker.cs
new file mode 100644
index 0000000..92098ed
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSMServerConnectionWorker.cs
@@ -0,0 +1,107 @@
+using JMayer.Net;
+
+namespace JMayer.Example.WindowsService;
+
+///
+/// The class manages starting/stopping the server & receiving remote connections from the clients.
+///
+///
+/// The server code is split between two workers because you want to accept client connections
+/// as frequently as possible; on a real server, multiple client connections may be queued to be
+/// accepted but if the worker accepts a connection, does other stuff and then sleeps for a second
+/// or more, the queued clients are missing BSMs.
+///
+internal class BSMServerConnectionWorker : BackgroundService
+{
+ ///
+ /// Used to log activity for the service.
+ ///
+ private readonly ILogger _logger;
+
+ ///
+ /// Used to manage TCP/IP communication.
+ ///
+ private readonly IServer _server;
+
+ ///
+ /// The port to monitor.
+ ///
+ public const int Port = 55555;
+
+ ///
+ /// The dependency injection constructor.
+ ///
+ /// Used to log activity for the service.
+ /// Used to manage TCP/IP communication.
+ public BSMServerConnectionWorker(ILogger logger, IServer server)
+ {
+ _logger = logger;
+ _server = server;
+ }
+
+ ///
+ /// The method handles starting the server & receiving remote connections from the clients.
+ ///
+ /// Used to cancel the task when the service stops.
+ /// A Task object for the async.
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ //Start the server if not ready.
+ if (!_server.IsReady)
+ {
+ try
+ {
+ _server.Start(Port);
+ _logger.LogInformation("The BSM server is listen on {Port} port.", Port);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "The BSM server failed to listen to the {Port} port", Port);
+ }
+ }
+ //Accept the client connections.
+ else
+ {
+ try
+ {
+ Guid id = await _server.AcceptIncomingConnectionAsync(stoppingToken);
+
+ if (id != Guid.Empty)
+ {
+ _logger.LogInformation("The BSM server accepted a remote client connection.");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "The BSM server failed to accept a remote client connection.");
+ }
+ }
+
+ await Task.Delay(10, stoppingToken);
+ }
+ }
+
+ ///
+ ///
+ /// This is overriden so the server can stop listening on the port.
+ ///
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ if (_server.IsReady)
+ {
+ try
+ {
+ _server.Stop();
+ _logger.LogInformation("The BSM server has stopped.");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "The BSM server failed to stop; the service will still stop.");
+ }
+ }
+
+ await base.StopAsync(cancellationToken);
+ }
+}
diff --git a/JMayer.Example.WindowsService/BSMServerOutputWorker.cs b/JMayer.Example.WindowsService/BSMServerOutputWorker.cs
new file mode 100644
index 0000000..7ccadd7
--- /dev/null
+++ b/JMayer.Example.WindowsService/BSMServerOutputWorker.cs
@@ -0,0 +1,86 @@
+using JMayer.Example.WindowsService.BSM;
+using JMayer.Net;
+
+namespace JMayer.Example.WindowsService;
+
+///
+/// The class manages stale connections and sending the BSMs to the clients.
+///
+internal class BSMServerOutputWorker : BackgroundService
+{
+ ///
+ /// Used to generate BSMs.
+ ///
+ private readonly BSMGenerator _bsmGenerator;
+
+ ///
+ /// Used to log activity for the service.
+ ///
+ private readonly ILogger _logger;
+
+ ///
+ /// Used to manage TCP/IP communication.
+ ///
+ private readonly IServer _server;
+
+ ///
+ /// The dependency injection constructor.
+ ///
+ /// Used to generate BSMs.
+ /// Used to log activity for the service.
+ /// Used to manage TCP/IP communication.
+ public BSMServerOutputWorker(BSMGenerator bsmGenerator, ILogger logger, IServer server)
+ {
+ _bsmGenerator = bsmGenerator;
+ _logger = logger;
+ _server = server;
+ }
+
+ ///
+ /// The class manages client connections and sending BSMs to the clients.
+ ///
+ /// Used to cancel the task when the service stops.
+ /// A Task object for the async.
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ if (_server.IsReady && _server.ConnectionCount > 0)
+ {
+ //Generate a BSM & sends it to the remote clients.
+ try
+ {
+ BSMPDU pdu = new()
+ {
+ BSM = _bsmGenerator.Generate(),
+ };
+ await _server.SendToAllAsync(pdu, stoppingToken);
+ _logger.LogInformation("The BSM server sent a BSM to the remote clients. {BSM}", pdu.BSM.ToTypeB());
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "The BSM server failed to send the BSM to the remote clients.");
+ }
+
+ //Manage stale remote clients.
+ List ids = _server.GetStaleRemoteConnections();
+
+ if (ids.Count > 0)
+ {
+ _logger.LogInformation("The BSM server detected stale remote clients; will attempt to disconnect.");
+
+ foreach (Guid id in ids)
+ {
+ try
+ {
+ _server.Disconnect(id);
+ }
+ catch { }
+ }
+ }
+ }
+
+ await Task.Delay(5_000, stoppingToken);
+ }
+ }
+}
diff --git a/JMayer.Example.WindowsService/Program.cs b/JMayer.Example.WindowsService/Program.cs
index a2c8451..2990674 100644
--- a/JMayer.Example.WindowsService/Program.cs
+++ b/JMayer.Example.WindowsService/Program.cs
@@ -1,7 +1,21 @@
using JMayer.Example.WindowsService;
+using JMayer.Example.WindowsService.BSM;
+using JMayer.Net;
+using JMayer.Net.TcpIp;
var builder = Host.CreateApplicationBuilder(args);
-builder.Services.AddHostedService();
+builder.Services.AddWindowsService(options =>
+{
+ options.ServiceName = "Example BSM Service";
+});
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton(new TcpIpClient(new BSMParser()));
+builder.Services.AddSingleton(new TcpIpServer(new BSMParser()) { ConnectionStaleMode = ConnectionStaleMode.LastSent, ConnectionTimeout = 60 });
+builder.Services.AddHostedService();
+builder.Services.AddHostedService();
+builder.Services.AddHostedService();
+
var host = builder.Build();
host.Run();
diff --git a/JMayer.Example.WindowsService/Worker.cs b/JMayer.Example.WindowsService/Worker.cs
deleted file mode 100644
index b15d9f2..0000000
--- a/JMayer.Example.WindowsService/Worker.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace JMayer.Example.WindowsService
-{
- public class Worker : BackgroundService
- {
- private readonly ILogger _logger;
-
- public Worker(ILogger logger)
- {
- _logger = logger;
- }
-
- protected override async Task ExecuteAsync(CancellationToken stoppingToken)
- {
- while (!stoppingToken.IsCancellationRequested)
- {
- if (_logger.IsEnabled(LogLevel.Information))
- {
- _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
- }
- await Task.Delay(1000, stoppingToken);
- }
- }
- }
-}
diff --git a/TestProject/Test/BSMGeneratorUnitTest.cs b/TestProject/Test/BSMGeneratorUnitTest.cs
new file mode 100644
index 0000000..c6f1ee0
--- /dev/null
+++ b/TestProject/Test/BSMGeneratorUnitTest.cs
@@ -0,0 +1,205 @@
+using JMayer.Example.WindowsService.BSM;
+
+namespace TestProject.Test;
+
+///
+/// The class manages testing the BSM generator.
+///
+public class BSMGeneratorUnitTest
+{
+ ///
+ /// The constant for the maximum number of airlines used by the BSM generator.
+ ///
+ private const int MaxAirline = 4;
+
+ ///
+ /// The method verifies the generator will rollover to the first airline after the maximum number is generated.
+ ///
+ [Fact]
+ public void VerifyAirlineRollover()
+ {
+ BSM bsm = new();
+ BSMGenerator bsmGenerator = new();
+ int maxPassengers = BSMGenerator.MaxPassengerCount * MaxAirline;
+
+ for (int index = 0; index <= maxPassengers; index++)
+ {
+ bsm = bsmGenerator.Generate();
+ }
+
+ //Verify the baggage tag details.
+ Assert.NotNull(bsm.BaggageTagDetails);
+ Assert.Single(bsm.BaggageTagDetails.BaggageTagNumbers);
+ Assert.Equal($"0{BSMGenerator.AmericanAirlinesNumericCode}{(BSMGenerator.MaxPassengerCount + 1).ToString().PadLeft(6, '0')}", bsm.BaggageTagDetails.BaggageTagNumbers[0]);
+
+ //Verify the change of status.
+ Assert.Equal(BSM.Add, bsm.ChangeOfStatus);
+
+ //Verify the outbound flight.
+ Assert.NotNull(bsm.OutboundFlight);
+ Assert.Equal(BSMGenerator.AmericanAirlineAlphaCode, bsm.OutboundFlight.Airline);
+ Assert.NotEmpty(bsm.OutboundFlight.ClassOfTravel); //Class of Travel is randomly set so just make sure its set to something.
+ Assert.NotEmpty(bsm.OutboundFlight.Destination); //Destination is randomly set so just make sure its set to something.
+ Assert.Equal(DateTime.Today.ToDayMonthFormat(), bsm.OutboundFlight.FlightDate);
+ Assert.Equal((MaxAirline + 1).ToString().PadLeft(4, '0'), bsm.OutboundFlight.FlightNumber);
+
+ //Verify the passenger name.
+ Assert.NotNull(bsm.PassengerName);
+ Assert.Single(bsm.PassengerName.GivenNames);
+ Assert.Equal($"{BSMGenerator.DotPGivenName}1", bsm.PassengerName.GivenNames[0]);
+ Assert.Equal(BSMGenerator.DotPSurName, bsm.PassengerName.SurName);
+
+ //Verify the version supplementary data.
+ Assert.NotNull(bsm.VersionSupplementaryData);
+ Assert.Equal(BSMGenerator.DotVAirportCode, bsm.VersionSupplementaryData.AirportCode);
+ Assert.Equal(VersionSupplementaryData.LocalBaggageSourceIndicator, bsm.VersionSupplementaryData.BaggageSourceIndicator);
+ Assert.Equal(BSMGenerator.DotVDataDictionaryVersionNumber, bsm.VersionSupplementaryData.DataDictionaryVersionNumber);
+ }
+
+ ///
+ /// The method verifies the generator will rollover the flight number after the maximum number is generated.
+ ///
+ [Fact]
+ public void VerifyFlightNumberRollover()
+ {
+ BSM bsm = new();
+ BSMGenerator bsmGenerator = new();
+ int maxPassengers = BSMGenerator.MaxPassengerCount * BSMGenerator.MaxFlightNumber;
+
+ for (int index = 0; index <= maxPassengers; index++)
+ {
+ bsm = bsmGenerator.Generate();
+ }
+
+ //Verify the baggage tag details.
+ Assert.NotNull(bsm.BaggageTagDetails);
+ Assert.Single(bsm.BaggageTagDetails.BaggageTagNumbers);
+ Assert.NotEmpty(bsm.BaggageTagDetails.BaggageTagNumbers[0]); //999,900 passengers are generated and I don't think there's an easy to know what the IATA number will be so just make sure its set.
+
+ //Verify the change of status.
+ Assert.Equal(BSM.Add, bsm.ChangeOfStatus);
+
+ //Verify the outbound flight.
+ Assert.NotNull(bsm.OutboundFlight);
+ Assert.NotEmpty(bsm.OutboundFlight.Airline); //999,900 passengers are generated and I don't think there's an easy to know what the airline will be so just make sure its set.
+ Assert.NotEmpty(bsm.OutboundFlight.ClassOfTravel); //Class of Travel is randomly set so just make sure its set to something.
+ Assert.NotEmpty(bsm.OutboundFlight.Destination); //Destination is randomly set so just make sure its set to something.
+ Assert.Equal(DateTime.Today.ToDayMonthFormat(), bsm.OutboundFlight.FlightDate);
+ Assert.Equal(BSMGenerator.MinFlightNumber.ToString().PadLeft(4, '0'), bsm.OutboundFlight.FlightNumber);
+
+ //Verify the passenger name.
+ Assert.NotNull(bsm.PassengerName);
+ Assert.Single(bsm.PassengerName.GivenNames);
+ Assert.Equal($"{BSMGenerator.DotPGivenName}1", bsm.PassengerName.GivenNames[0]);
+ Assert.Equal(BSMGenerator.DotPSurName, bsm.PassengerName.SurName);
+
+ //Verify the version supplementary data.
+ Assert.NotNull(bsm.VersionSupplementaryData);
+ Assert.Equal(BSMGenerator.DotVAirportCode, bsm.VersionSupplementaryData.AirportCode);
+ Assert.Equal(VersionSupplementaryData.LocalBaggageSourceIndicator, bsm.VersionSupplementaryData.BaggageSourceIndicator);
+ Assert.Equal(BSMGenerator.DotVDataDictionaryVersionNumber, bsm.VersionSupplementaryData.DataDictionaryVersionNumber);
+ }
+
+ ///
+ /// The class verifies a BSM is generated.
+ ///
+ [Fact]
+ public void VerifyGeneration()
+ {
+ BSMGenerator bsmGenerator = new();
+ BSM bsm = bsmGenerator.Generate();
+
+ //Verify the baggage tag details.
+ Assert.NotNull(bsm.BaggageTagDetails);
+ Assert.Single(bsm.BaggageTagDetails.BaggageTagNumbers);
+ Assert.Equal($"0{BSMGenerator.AmericanAirlinesNumericCode}{IATAGenerator.MinSequenceNumber.ToString().PadLeft(6, '0')}", bsm.BaggageTagDetails.BaggageTagNumbers[0]);
+
+ //Verify the change of status.
+ Assert.Equal(BSM.Add, bsm.ChangeOfStatus);
+
+ //Verify the outbound flight.
+ Assert.NotNull(bsm.OutboundFlight);
+ Assert.Equal(BSMGenerator.AmericanAirlineAlphaCode, bsm.OutboundFlight.Airline);
+ Assert.NotEmpty(bsm.OutboundFlight.ClassOfTravel); //Class of Travel is randomly set so just make sure its set to something.
+ Assert.NotEmpty(bsm.OutboundFlight.Destination); //Destination is randomly set so just make sure its set to something.
+ Assert.Equal(DateTime.Today.ToDayMonthFormat(), bsm.OutboundFlight.FlightDate);
+ Assert.Equal(BSMGenerator.MinFlightNumber.ToString().PadLeft(4, '0'), bsm.OutboundFlight.FlightNumber);
+
+ //Verify the passenger name.
+ Assert.NotNull(bsm.PassengerName);
+ Assert.Single(bsm.PassengerName.GivenNames);
+ Assert.Equal($"{BSMGenerator.DotPGivenName}1", bsm.PassengerName.GivenNames[0]);
+ Assert.Equal(BSMGenerator.DotPSurName, bsm.PassengerName.SurName);
+
+ //Verify the version supplementary data.
+ Assert.NotNull(bsm.VersionSupplementaryData);
+ Assert.Equal(BSMGenerator.DotVAirportCode, bsm.VersionSupplementaryData.AirportCode);
+ Assert.Equal(VersionSupplementaryData.LocalBaggageSourceIndicator, bsm.VersionSupplementaryData.BaggageSourceIndicator);
+ Assert.Equal(BSMGenerator.DotVDataDictionaryVersionNumber, bsm.VersionSupplementaryData.DataDictionaryVersionNumber);
+ }
+
+ ///
+ /// The method verifies the passenger increments by one on each generation.
+ ///
+ [Fact]
+ public void VerifyPassengerIncrementsByOne()
+ {
+ BSMGenerator bsmGenerator = new();
+ BSM bsm1 = bsmGenerator.Generate();
+ BSM bsm2 = bsmGenerator.Generate();
+ BSM bsm3 = bsmGenerator.Generate();
+
+ Assert.NotNull(bsm1.PassengerName);
+ Assert.Equal($"{BSMGenerator.DotPGivenName}1", bsm1.PassengerName.GivenNames[0]);
+
+ Assert.NotNull(bsm2.PassengerName);
+ Assert.Equal($"{BSMGenerator.DotPGivenName}2", bsm2.PassengerName.GivenNames[0]);
+
+ Assert.NotNull(bsm3.PassengerName);
+ Assert.Equal($"{BSMGenerator.DotPGivenName}3", bsm3.PassengerName.GivenNames[0]);
+ }
+
+ ///
+ /// The method verifies the generator will rollover the passengers after the maximum number is generated. It also verifies a new airline and flight number are generated.
+ ///
+ [Fact]
+ public void VerifyPassengerRollover()
+ {
+ BSM bsm = new();
+ BSMGenerator bsmGenerator = new();
+
+ for (int index = 0; index <= BSMGenerator.MaxPassengerCount; index++)
+ {
+ bsm = bsmGenerator.Generate();
+ }
+
+ //Verify the baggage tag details.
+ Assert.NotNull(bsm.BaggageTagDetails);
+ Assert.Single(bsm.BaggageTagDetails.BaggageTagNumbers);
+ //The sequence used by the BSM generator is 001, 006, 016, 526 so on the first passenger rollover, 006 will be used next.
+ Assert.Equal($"0{BSMGenerator.DeltaNumericCode}{IATAGenerator.MinSequenceNumber.ToString().PadLeft(6, '0')}", bsm.BaggageTagDetails.BaggageTagNumbers[0]);
+
+ //Verify the change of status.
+ Assert.Equal(BSM.Add, bsm.ChangeOfStatus);
+
+ //Verify the outbound flight.
+ Assert.NotNull(bsm.OutboundFlight);
+ Assert.Equal(BSMGenerator.DeltaAlphaCode, bsm.OutboundFlight.Airline); //The sequence used by the BSM generator is AA, DL, UA, WN so on the first passenger rollover, DL will be used next.
+ Assert.NotEmpty(bsm.OutboundFlight.ClassOfTravel); //Class of Travel is randomly set so just make sure its set to something.
+ Assert.NotEmpty(bsm.OutboundFlight.Destination); //Destination is randomly set so just make sure its set to something.
+ Assert.Equal(DateTime.Today.ToDayMonthFormat(), bsm.OutboundFlight.FlightDate);
+ Assert.Equal((BSMGenerator.MinFlightNumber + 1).ToString().PadLeft(4, '0'), bsm.OutboundFlight.FlightNumber);
+
+ //Verify the passenger name.
+ Assert.NotNull(bsm.PassengerName);
+ Assert.Single(bsm.PassengerName.GivenNames);
+ Assert.Equal($"{BSMGenerator.DotPGivenName}1", bsm.PassengerName.GivenNames[0]);
+ Assert.Equal(BSMGenerator.DotPSurName, bsm.PassengerName.SurName);
+
+ //Verify the version supplementary data.
+ Assert.NotNull(bsm.VersionSupplementaryData);
+ Assert.Equal(BSMGenerator.DotVAirportCode, bsm.VersionSupplementaryData.AirportCode);
+ Assert.Equal(VersionSupplementaryData.LocalBaggageSourceIndicator, bsm.VersionSupplementaryData.BaggageSourceIndicator);
+ Assert.Equal(BSMGenerator.DotVDataDictionaryVersionNumber, bsm.VersionSupplementaryData.DataDictionaryVersionNumber);
+ }
+}
diff --git a/TestProject/Test/BSMParserUnitTest.cs b/TestProject/Test/BSMParserUnitTest.cs
new file mode 100644
index 0000000..d52f6a8
--- /dev/null
+++ b/TestProject/Test/BSMParserUnitTest.cs
@@ -0,0 +1,930 @@
+using JMayer.Example.WindowsService.BSM;
+using JMayer.Net.ProtocolDataUnit;
+using System.Text;
+
+namespace TestProject.Test;
+
+///
+/// The class manages testing the BSM parser.
+///
+public class BSMParserUnitTest
+{
+ ///
+ /// The constant for the .F airline.
+ ///
+ private const string DotFAirline = "AA";
+
+ ///
+ /// The constant for the .F class of travel.
+ ///
+ private const string DotFClassOfTravel = "A";
+
+ ///
+ /// The constant for the .F destination.
+ ///
+ private const string DotFDestination = "MSY";
+
+ ///
+ /// The constant for the .F flight number.
+ ///
+ private const string DotFFlightNumber = "1234";
+
+ ///
+ /// The constant for the .F invalid flight date.
+ ///
+ private const string DotFInvalidFlightDate = "JAN01";
+
+ ///
+ /// The constant for the .F invalid flight number.
+ ///
+ private const string DotFInvalidFlightNumber = "ABCD";
+
+ ///
+ /// The constant for the .N tag number.
+ ///
+ private const string DotNTagNumber = "0001123456";
+
+ ///
+ /// The constant for the .P given name.
+ ///
+ private const string DotPGivenName = "PASSENGER";
+
+ ///
+ /// The constant for the .P surname.
+ ///
+ private const string DotPSurName = "TEST";
+
+ ///
+ /// The constant for the .V airport code.
+ ///
+ private const string DotVAirportCode = "MCO";
+
+ ///
+ /// The constant for the .V data dictionary version number.
+ ///
+ private const int DotVDataDictionaryVersionNumber = 1;
+
+ ///
+ /// The constant for the .V invalid baggage source indicator.
+ ///
+ private const string DotVInvalidBaggageSourceIndicator = "A";
+
+ ///
+ /// The constant for the .V invalid data dictionary version number.
+ ///
+ private const int DotVInvalidDataDictionaryVersionNumber = 0;
+
+ ///
+ /// The method verifies a BSM with all fields set can be parsed.
+ ///
+ [Fact]
+ public void VerifyAllFields()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.True(result.PDUs[0].IsValid, "The BSM is not valid.");
+ Assert.True(new BMSEqualityComparer().Equals(bsm, ((BSMPDU)result.PDUs[0]).BSM), "The BSM does not equal the BSM in the parser results.");
+ }
+
+ ///
+ /// The method verifies a BSM with a bad .F airline will cause a validation issue.
+ ///
+ [Fact]
+ public void VerifyDotFAirlineValidation()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline.ToLower(),
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.False(result.PDUs[0].IsValid, "The BSM is valid. It's expected to be invalid.");
+ Assert.Single(result.PDUs[0].ValidationResults);
+ Assert.Contains(nameof(OutboundFlight.Airline), result.PDUs[0].ValidationResults.First().MemberNames);
+ }
+
+ ///
+ /// The method verifies a BSM with a bad .F class of travel will cause a validation issue.
+ ///
+ [Fact]
+ public void VerifyDotFClassOfTravelValidation()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel.ToLower(),
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.False(result.PDUs[0].IsValid, "The BSM is valid. It's expected to be invalid.");
+ Assert.Single(result.PDUs[0].ValidationResults);
+ Assert.Contains(nameof(OutboundFlight.ClassOfTravel), result.PDUs[0].ValidationResults.First().MemberNames);
+ }
+
+ ///
+ /// The method verifies a BSM with a bad .F destination will cause a validation issue.
+ ///
+ [Fact]
+ public void VerifyDotFDestinationValidation()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination.ToLower(),
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.False(result.PDUs[0].IsValid, "The BSM is valid. It's expected to be invalid.");
+ Assert.Single(result.PDUs[0].ValidationResults);
+ Assert.Contains(nameof(OutboundFlight.Destination), result.PDUs[0].ValidationResults.First().MemberNames);
+ }
+
+ ///
+ /// The method verifies a BSM with a bad .F flight date will cause a validation issue.
+ ///
+ [Fact]
+ public void VerifyDotFFlightDateValidation()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DotFInvalidFlightDate,
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.False(result.PDUs[0].IsValid, "The BSM is valid. It's expected to be invalid.");
+ Assert.Single(result.PDUs[0].ValidationResults);
+ Assert.Contains(nameof(OutboundFlight.FlightDate), result.PDUs[0].ValidationResults.First().MemberNames);
+ }
+
+ ///
+ /// The method verifies a BSM with a bad .F flight number will cause a validation issue.
+ ///
+ [Fact]
+ public void VerifyDotFFlightNumberValidation()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFInvalidFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.False(result.PDUs[0].IsValid, "The BSM is valid. It's expected to be invalid.");
+ Assert.Single(result.PDUs[0].ValidationResults);
+ Assert.Contains(nameof(OutboundFlight.FlightNumber), result.PDUs[0].ValidationResults.First().MemberNames);
+ }
+
+ ///
+ /// The method verifies a BSM with a .F element with no class of travel can be parsed.
+ ///
+ [Fact]
+ public void VerifyDotFNoClassOfTravel()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.True(result.PDUs[0].IsValid, "The BSM is not valid.");
+ Assert.True(new BMSEqualityComparer().Equals(bsm, ((BSMPDU)result.PDUs[0]).BSM), "The BSM does not equal the BSM in the parser results.");
+ }
+
+ ///
+ /// The method verifies a BSM with a .F element with no destination can be parsed; class of travel
+ /// will also not be set.
+ ///
+ [Fact]
+ public void VerifyDotFNoDestination()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.True(result.PDUs[0].IsValid, "The BSM is not valid.");
+ Assert.True(new BMSEqualityComparer().Equals(bsm, ((BSMPDU)result.PDUs[0]).BSM), "The BSM does not equal the BSM in the parser results.");
+ }
+
+ ///
+ /// The method verifies a BSM with a .N element with multiple tags can be parsed.
+ ///
+ [Fact]
+ public void VerifyDotNMultipleTags()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber, "0001123457", "0001123458", "0001123459", "0001123460"],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.True(result.PDUs[0].IsValid, "The BSM is not valid.");
+ Assert.True(new BMSEqualityComparer().Equals(bsm, ((BSMPDU)result.PDUs[0]).BSM), "The BSM does not equal the BSM in the parser results.");
+ }
+
+ ///
+ /// The method verifies a BSM with a badly formatted .N will cause a validation issue.
+ ///
+ [Fact]
+ public void VerifyDotNTagValidation()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ //Increasing Count from 001 to 0001 to force .N to have too many characters. That will create a range validation issue
+ //because the parser will fail to parse the .N element which leave the list empty.
+ bsmString = bsmString.Replace($"{DotNTagNumber}{bsm.BaggageTagDetails.Count:D3}", $"{DotNTagNumber}{bsm.BaggageTagDetails.Count:D4}");
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.False(result.PDUs[0].IsValid, "The BSM is valid. It's expected to be invalid.");
+ Assert.Single(result.PDUs[0].ValidationResults);
+ Assert.Contains(nameof(BaggageTagDetails.BaggageTagNumbers), result.PDUs[0].ValidationResults.First().MemberNames);
+ }
+
+ ///
+ /// The method verifies a BSM with a .P element with multiple given names can be parsed.
+ ///
+ [Fact]
+ public void VerifyDotPMultipleGivenNames()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = ["Given Name 1", "Given Name 2", "Given Name 3"],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.True(result.PDUs[0].IsValid, "The BSM is not valid.");
+ Assert.True(new BMSEqualityComparer().Equals(bsm, ((BSMPDU)result.PDUs[0]).BSM), "The BSM does not equal the BSM in the parser results.");
+ }
+
+ ///
+ /// The method verifies a BSM with a .P element with no given names can be parsed.
+ ///
+ [Fact]
+ public void VerifyDotPNoGivenNames()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.True(result.PDUs[0].IsValid, "The BSM is not valid.");
+ Assert.True(new BMSEqualityComparer().Equals(bsm, ((BSMPDU)result.PDUs[0]).BSM), "The BSM does not equal the BSM in the parser results.");
+ }
+
+ ///
+ /// The method verifies a BSM with a bad .P surname will cause a validation issue.
+ ///
+ [Fact]
+ public void VerifyDotPSurNameValidation()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = string.Empty,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.False(result.PDUs[0].IsValid, "The BSM is valid. It's expected to be invalid.");
+ Assert.Single(result.PDUs[0].ValidationResults);
+ Assert.Contains(nameof(PassengerName.SurName), result.PDUs[0].ValidationResults.First().MemberNames);
+ }
+
+ ///
+ /// The method verifies a BSM with a bad .V airport code will cause a validation issue.
+ ///
+ [Fact]
+ public void VerifyDotVAirportCodeValidation()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode.ToLower(),
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.False(result.PDUs[0].IsValid, "The BSM is valid. It's expected to be invalid.");
+ Assert.Single(result.PDUs[0].ValidationResults);
+ Assert.Contains(nameof(VersionSupplementaryData.AirportCode), result.PDUs[0].ValidationResults.First().MemberNames);
+ }
+
+ ///
+ /// The method verifies a BSM with a bad .V baggage source indicator will cause a validation issue.
+ ///
+ [Fact]
+ public void VerifyDotVBaggageSourceIndicatorValidation()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = DotVInvalidBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.False(result.PDUs[0].IsValid, "The BSM is valid. It's expected to be invalid.");
+ Assert.Single(result.PDUs[0].ValidationResults);
+ Assert.Contains(nameof(VersionSupplementaryData.BaggageSourceIndicator), result.PDUs[0].ValidationResults.First().MemberNames);
+ }
+
+ ///
+ /// The method verifies a BSM with a bad .V data dictionary version number will cause a validation issue.
+ ///
+ [Fact]
+ public void VerifyDotVDataDictionaryVersionNumberValidation()
+ {
+ BSM bsm = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [DotPGivenName],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVInvalidDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Single(result.PDUs);
+ Assert.IsType(result.PDUs[0]);
+ Assert.False(result.PDUs[0].IsValid, "The BSM is valid. It's expected to be invalid.");
+ Assert.Single(result.PDUs[0].ValidationResults);
+ Assert.Contains(nameof(VersionSupplementaryData.DataDictionaryVersionNumber), result.PDUs[0].ValidationResults.First().MemberNames);
+ }
+
+ ///
+ /// The method verfies multiple BSMs can be parsed.
+ ///
+ [Fact]
+ public void VerifyMultipleBSMs()
+ {
+ BSM bsm1 = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = [DotNTagNumber],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [$"{DotPGivenName}1"],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSM bsm2 = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = ["0001123457"],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [$"{DotPGivenName}2"],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSM bsm3 = new()
+ {
+ BaggageTagDetails = new()
+ {
+ BaggageTagNumbers = ["0001123458"],
+ },
+ ChangeOfStatus = BSM.Add,
+ OutboundFlight = new()
+ {
+ Airline = DotFAirline,
+ ClassOfTravel = DotFClassOfTravel,
+ Destination = DotFDestination,
+ FlightDate = DateTime.Today.ToDayMonthFormat(),
+ FlightNumber = DotFFlightNumber,
+ },
+ PassengerName = new()
+ {
+ GivenNames = [$"{DotPGivenName}2"],
+ SurName = DotPSurName,
+ },
+ VersionSupplementaryData = new()
+ {
+ AirportCode = DotVAirportCode,
+ BaggageSourceIndicator = VersionSupplementaryData.LocalBaggageSourceIndicator,
+ DataDictionaryVersionNumber = DotVDataDictionaryVersionNumber,
+ },
+ };
+
+ BSMParser parser = new();
+ string bsmString = bsm1.ToTypeB() + bsm2.ToTypeB() + bsm3.ToTypeB();
+ byte[] bsmBytes = Encoding.ASCII.GetBytes(bsmString);
+
+ PDUParserResult result = parser.Parse(bsmBytes);
+
+ Assert.Equal(3, result.PDUs.Count);
+ Assert.IsType(result.PDUs[0]);
+ Assert.IsType(result.PDUs[1]);
+ Assert.IsType(result.PDUs[2]);
+ Assert.True(result.PDUs[0].IsValid, "BSM 1 is not valid.");
+ Assert.True(result.PDUs[1].IsValid, "BSM 2 is not valid.");
+ Assert.True(result.PDUs[2].IsValid, "BSM 3 is not valid.");
+ Assert.True(new BMSEqualityComparer().Equals(bsm1, ((BSMPDU)result.PDUs[0]).BSM), "BSM 1 does not equal the first BSM in the parser results.");
+ Assert.True(new BMSEqualityComparer().Equals(bsm2, ((BSMPDU)result.PDUs[1]).BSM), "BSM 2 does not equal the second BSM in the parser results.");
+ Assert.True(new BMSEqualityComparer().Equals(bsm3, ((BSMPDU)result.PDUs[2]).BSM), "BSM 3 does not equal the third BSM in the parser results.");
+ }
+}
diff --git a/TestProject/Test/IATAGeneratorUnitTest.cs b/TestProject/Test/IATAGeneratorUnitTest.cs
new file mode 100644
index 0000000..6618b8c
--- /dev/null
+++ b/TestProject/Test/IATAGeneratorUnitTest.cs
@@ -0,0 +1,69 @@
+using JMayer.Example.WindowsService.BSM;
+
+namespace TestProject.Test;
+
+///
+/// The class manages testing the IATA generator.
+///
+public class IATAGeneratorUnitTest
+{
+ ///
+ /// The constant for the airline numeric code.
+ ///
+ private const string AirlineNumericCode = "001";
+
+ ///
+ /// The method verifies an IATA will be generated.
+ ///
+ [Fact]
+ public void VerifyGeneration()
+ {
+ IATAGenerator iataGenerator = new()
+ {
+ AirlineNumericCode = AirlineNumericCode,
+ };
+ string iata = iataGenerator.Generate();
+
+ Assert.Equal($"0{iataGenerator.AirlineNumericCode}{IATAGenerator.MinSequenceNumber.ToString().PadLeft(6, '0')}", iata);
+ }
+
+ ///
+ /// The method verifies if 3 IATAs are generated, each is incremented by 1.
+ ///
+ [Fact]
+ public void VerifyIncrementByOne()
+ {
+ IATAGenerator iataGenerator = new()
+ {
+ AirlineNumericCode = AirlineNumericCode,
+ };
+
+ string iata1 = iataGenerator.Generate();
+ string iata2 = iataGenerator.Generate();
+ string iata3 = iataGenerator.Generate();
+
+ Assert.Equal($"0{iataGenerator.AirlineNumericCode}{IATAGenerator.MinSequenceNumber.ToString().PadLeft(6, '0')}", iata1);
+ Assert.Equal($"0{iataGenerator.AirlineNumericCode}{(IATAGenerator.MinSequenceNumber + 1).ToString().PadLeft(6, '0')}", iata2);
+ Assert.Equal($"0{iataGenerator.AirlineNumericCode}{(IATAGenerator.MinSequenceNumber + 2).ToString().PadLeft(6, '0')}", iata3);
+ }
+
+ ///
+ /// The method verifies the generator will rollover after 999999 IATAs are generated.
+ ///
+ [Fact]
+ public void VerifyRollover()
+ {
+ string iata = string.Empty;
+ IATAGenerator iataGenerator = new()
+ {
+ AirlineNumericCode = AirlineNumericCode,
+ };
+
+ for (int index = 0; index <= IATAGenerator.MaxSequenceNumber; index++)
+ {
+ iata = iataGenerator.Generate();
+ }
+
+ Assert.Equal($"0{iataGenerator.AirlineNumericCode}{IATAGenerator.MinSequenceNumber.ToString().PadLeft(6, '0')}", iata);
+ }
+}
diff --git a/TestProject/TestProject.csproj b/TestProject/TestProject.csproj
new file mode 100644
index 0000000..53e930b
--- /dev/null
+++ b/TestProject/TestProject.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/workflows/mainworkflow.yml b/workflows/mainworkflow.yml
new file mode 100644
index 0000000..c8c3466
--- /dev/null
+++ b/workflows/mainworkflow.yml
@@ -0,0 +1,29 @@
+# This workflow will build a .NET project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
+
+name: MainWorkflow
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --configuration Release --no-restore
+ - name: Test
+ run: dotnet test --verbosity normal