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