diff --git a/.github/workflows/mainworkflow.yml b/.github/workflows/mainworkflow.yml index c8c3466..ba28f55 100644 --- a/.github/workflows/mainworkflow.yml +++ b/.github/workflows/mainworkflow.yml @@ -20,7 +20,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/JMayer.Example.ASPReact.Server/Airlines/Airline.cs b/JMayer.Example.ASPReact.Server/Airlines/Airline.cs index bcf9280..ee9ff32 100644 --- a/JMayer.Example.ASPReact.Server/Airlines/Airline.cs +++ b/JMayer.Example.ASPReact.Server/Airlines/Airline.cs @@ -6,7 +6,7 @@ namespace JMayer.Example.ASPReact.Server.Airlines; /// /// The class represents an airline and its codes. /// -public class Airline : UserEditableDataObject +public class Airline : DataObject { /// /// The property gets/sets the IATA code assigned by the IATA organization. @@ -21,6 +21,11 @@ public class Airline : UserEditableDataObject [RegularExpression("^[A-Z]{3}$", ErrorMessage = "The ICAO must be 3 capital letters.")] public string ICAO { get; set; } = string.Empty; + /// + /// Overridden to add Required data annotation. + [Required] + public override string? Name { get => base.Name; set => base.Name = value; } + /// /// The property gets/sets the number code assigned by the IATA organization. /// @@ -28,6 +33,16 @@ public class Airline : UserEditableDataObject [RegularExpression("^\\d{3}$", ErrorMessage = "The number code must be 3 digits.")] public string NumberCode { get; set; } = ZeroNumberCode; + /// + /// The property gets/sets the id for the default sort destintion assigned to the airline. + /// + public long SortDestinationID { get; set; } + + /// + /// The property gets/sets the name of the default sort destination assigned to the airline. + /// + public string SortDestinationName { get; set; } = string.Empty; + /// /// The constant for the zero number code. /// @@ -54,6 +69,8 @@ public override void MapProperties(DataObject dataObject) IATA = airline.IATA; ICAO = airline.ICAO; NumberCode = airline.NumberCode; + SortDestinationID = airline.SortDestinationID; + SortDestinationName = airline.SortDestinationName; } } } diff --git a/JMayer.Example.ASPReact.Server/Airlines/AirlineController.cs b/JMayer.Example.ASPReact.Server/Airlines/AirlineController.cs index d098e0a..30946ac 100644 --- a/JMayer.Example.ASPReact.Server/Airlines/AirlineController.cs +++ b/JMayer.Example.ASPReact.Server/Airlines/AirlineController.cs @@ -1,4 +1,4 @@ -using JMayer.Web.Mvc.Controller; +using JMayer.Web.Mvc.Controller.Api; using Microsoft.AspNetCore.Mvc; namespace JMayer.Example.ASPReact.Server.Airlines; @@ -8,8 +8,24 @@ namespace JMayer.Example.ASPReact.Server.Airlines; /// [Route("api/[controller]")] [ApiController] -public class AirlineController : UserEditableController +public class AirlineController : StandardCRUDController { /// public AirlineController(IAirlineDataLayer dataLayer, ILogger logger) : base(dataLayer, logger) { } + + /// + /// Overridden to hide the string version of this. The client doesn't use it and swagger complains about a conflict when its exposed. + [NonAction] + public override Task DeleteAsync(string id) + { + return base.DeleteAsync(id); + } + + /// + /// Overridden to hide the string version of this. The client doesn't use it and swagger complains about a conflict when its exposed. + [NonAction] + public override Task GetSingleAsync(string id) + { + return base.GetSingleAsync(id); + } } diff --git a/JMayer.Example.ASPReact.Server/Airlines/AirlineDataLayer.cs b/JMayer.Example.ASPReact.Server/Airlines/AirlineDataLayer.cs index 2c58588..9349797 100644 --- a/JMayer.Example.ASPReact.Server/Airlines/AirlineDataLayer.cs +++ b/JMayer.Example.ASPReact.Server/Airlines/AirlineDataLayer.cs @@ -1,4 +1,5 @@ using JMayer.Data.Database.DataLayer.MemoryStorage; +using JMayer.Example.ASPReact.Server.SortDestinations; using System.ComponentModel.DataAnnotations; namespace JMayer.Example.ASPReact.Server.Airlines; @@ -6,8 +7,25 @@ namespace JMayer.Example.ASPReact.Server.Airlines; /// /// The class manages CRUD interactions with the database for an airline. /// -public class AirlineDataLayer : UserEditableDataLayer, IAirlineDataLayer +public class AirlineDataLayer : StandardCRUDDataLayer, IAirlineDataLayer { + /// + /// The property gets/sets the data layer for accessing the sort desintation data. + /// + private readonly ISortDestinationDataLayer _sortDestinationDataLayer; + + /// + /// The dependency injection constructor. + /// + /// + public AirlineDataLayer(ISortDestinationDataLayer sortDestinationDataLayer) + { + IsOldDataObjectDetectionEnabled = true; + IsUniqueNameRequired = true; + + _sortDestinationDataLayer = sortDestinationDataLayer; + } + /// /// /// Overriden to check the ICAO is unique and the number code is unique (expect for 000). @@ -16,12 +34,17 @@ public override async Task> ValidateAsync(Airline dataObj { List validationResults = await base.ValidateAsync(dataObject, cancellationToken); - if (dataObject.ICAO != null && await ExistAsync(obj => obj.Integer64ID != dataObject.Integer64ID && obj.ICAO == dataObject.ICAO, cancellationToken) == true) + if (dataObject.ICAO is not null && await ExistAsync(obj => obj.Integer64ID != dataObject.Integer64ID && obj.ICAO == dataObject.ICAO, cancellationToken) is true) { validationResults.Add(new ValidationResult("The ICAO must be unique.", [nameof(Airline.ICAO)])); } - if (dataObject.NumberCode != Airline.ZeroNumberCode && await ExistAsync(obj => obj.Integer64ID != dataObject.Integer64ID && obj.NumberCode == dataObject.NumberCode, cancellationToken) == true) + if (await _sortDestinationDataLayer.ExistAsync(obj => obj.Integer64ID == dataObject.SortDestinationID, cancellationToken) is false) + { + validationResults.Add(new ValidationResult($"The {dataObject.SortDestinationID} sort destination was not found in the data store.", [nameof(Airline.SortDestinationID)])); + } + + if (dataObject.NumberCode is not Airline.ZeroNumberCode && await ExistAsync(obj => obj.Integer64ID != dataObject.Integer64ID && obj.NumberCode == dataObject.NumberCode, cancellationToken) is true) { validationResults.Add(new ValidationResult("The number code must be unique unless the code is 000.", [nameof(Airline.NumberCode)])); } diff --git a/JMayer.Example.ASPReact.Server/Airlines/AirlineEqualityComparer.cs b/JMayer.Example.ASPReact.Server/Airlines/AirlineEqualityComparer.cs index 9de47b2..f01a796 100644 --- a/JMayer.Example.ASPReact.Server/Airlines/AirlineEqualityComparer.cs +++ b/JMayer.Example.ASPReact.Server/Airlines/AirlineEqualityComparer.cs @@ -43,7 +43,7 @@ public AirlineEqualityComparer(bool excludeCreatedOn, bool excludeID, bool exclu /// public bool Equals(Airline? x, Airline? y) { - if (x == null || y == null) + if (x is null || y is null) { return false; } @@ -55,7 +55,8 @@ public bool Equals(Airline? x, Airline? y) && (_excludeID || x.Integer64ID == y.Integer64ID) && (_excludeLastEditedOn || x.LastEditedBy == y.LastEditedBy) && x.Name == y.Name - && x.NumberCode == y.NumberCode; + && x.NumberCode == y.NumberCode + && x.SortDestinationID == y.SortDestinationID; } /// diff --git a/JMayer.Example.ASPReact.Server/Airlines/IAirlineDataLayer.cs b/JMayer.Example.ASPReact.Server/Airlines/IAirlineDataLayer.cs index 0f856e8..e61e27d 100644 --- a/JMayer.Example.ASPReact.Server/Airlines/IAirlineDataLayer.cs +++ b/JMayer.Example.ASPReact.Server/Airlines/IAirlineDataLayer.cs @@ -5,6 +5,6 @@ namespace JMayer.Example.ASPReact.Server.Airlines; /// /// The interface for interacting with an airline collection in a database using CRUD operations. /// -public interface IAirlineDataLayer : IUserEditableDataLayer +public interface IAirlineDataLayer : IStandardCRUDDataLayer { } diff --git a/JMayer.Example.ASPReact.Server/FlightScheduleExampleBuilder.cs b/JMayer.Example.ASPReact.Server/FlightScheduleExampleBuilder.cs index 7f295b6..87b0a91 100644 --- a/JMayer.Example.ASPReact.Server/FlightScheduleExampleBuilder.cs +++ b/JMayer.Example.ASPReact.Server/FlightScheduleExampleBuilder.cs @@ -18,7 +18,17 @@ public class FlightScheduleExampleBuilder /// /// The property gets/sets the data layer used to interact with airlines. /// - public IAirlineDataLayer AirlineDataLayer { get; init; } = new AirlineDataLayer(); + public IAirlineDataLayer AirlineDataLayer { get; init; } + + /// + /// The constant for the American Airline IATA code. + /// + private const string AmericanIataCode = "AA"; + + /// + /// The constant for the Delta IATA code. + /// + private const string DeltaIataCode = "DL"; /// /// The property gets/sets the data layer used to ineract with the flights. @@ -35,11 +45,17 @@ public class FlightScheduleExampleBuilder /// public ISortDestinationDataLayer SortDestinationDataLayer { get; init; } = new SortDestinationDataLayer(); + /// + /// The constant for the Southwest IATA code. + /// + private const string SouthwestIataCode = "WN"; + /// /// The default constructor. /// public FlightScheduleExampleBuilder() { + AirlineDataLayer = new AirlineDataLayer(SortDestinationDataLayer); FlightDataLayer = new FlightDataLayer(AirlineDataLayer, GateDataLayer, SortDestinationDataLayer); GenerateAirportCodes(); } @@ -49,9 +65,9 @@ public FlightScheduleExampleBuilder() /// public void Build() { + BuildSortDestinations(); BuildAirlines(); BuildGates(); - BuildSortDestinations(); BuildFlights(); } @@ -60,26 +76,34 @@ public void Build() /// private void BuildAirlines() { + List sortDestinations = SortDestinationDataLayer.GetAllAsync().Result; + _ = AirlineDataLayer.CreateAsync(new Airline() { - IATA = "AA", + IATA = AmericanIataCode, ICAO = "AAL", Name = "American Airlines", NumberCode = "001", + SortDestinationID = sortDestinations[0].Integer64ID, + SortDestinationName = sortDestinations[0].Name ?? string.Empty, }); _ = AirlineDataLayer.CreateAsync(new Airline() { - IATA = "DL", + IATA = DeltaIataCode, ICAO = "DAL", Name = "Delta Air Lines", NumberCode = "006", + SortDestinationID = sortDestinations[2].Integer64ID, + SortDestinationName = sortDestinations[2].Name ?? string.Empty, }); _ = AirlineDataLayer.CreateAsync(new Airline() { - IATA = "WN", + IATA = SouthwestIataCode, ICAO = "SWA", Name = "Southwest Airlines", NumberCode = "526", + SortDestinationID = sortDestinations[4].Integer64ID, + SortDestinationName = sortDestinations[4].Name ?? string.Empty, }); } @@ -92,9 +116,16 @@ private void BuildFlights() List gates = GateDataLayer.GetAllAsync().Result; List sortDestinations = SortDestinationDataLayer.GetAllAsync().Result; + bool altAmericanGate = false; + bool altAmericanSortDestination = false; + + bool altDeltaGate = false; + bool altDeltaSortDestination = false; + + bool altSouthwestGate = false; + bool altSouthWestSortDestination = false; + int flightNumber = 1000; - int gateIndex = 0; - int sortDestinationIndex = 0; TimeSpan departTime = new(4, 0, 0); TimeSpan operationalEnd = new(22, 0, 0); @@ -102,6 +133,47 @@ private void BuildFlights() { foreach (Airline airline in airlines) { + Gate gate; + SortDestination? altSortDestination = null; + + //Determine which gate is used for the airline and if the alternative sort destination is used. + if (airline.IATA is AmericanIataCode) + { + gate = altAmericanGate ? gates[1] : gates[0]; + + if (altAmericanSortDestination) + { + altSortDestination = sortDestinations[1]; + } + + altAmericanGate = !altAmericanGate; + altAmericanSortDestination = !altAmericanSortDestination; + } + else if (airline.IATA is DeltaIataCode) + { + gate = altDeltaGate ? gates[3] : gates[2]; + + if (altDeltaSortDestination) + { + altSortDestination = sortDestinations[3]; + } + + altDeltaGate = !altDeltaGate; + altDeltaSortDestination = !altDeltaSortDestination; + } + else + { + gate = altSouthwestGate ? gates[5] : gates[4]; + + if (altSouthWestSortDestination) + { + altSortDestination = sortDestinations[5]; + } + + altSouthwestGate = !altSouthwestGate; + altSouthWestSortDestination = !altSouthWestSortDestination; + } + _ = FlightDataLayer.CreateAsync(new Flight() { AirlineIATACode = airline.IATA, @@ -110,27 +182,15 @@ private void BuildFlights() Destination = _airportCodes[new Random(DateTime.Now.Millisecond).Next(0, _airportCodes.Count - 1)], DepartTime = departTime, FlightNumber = flightNumber.ToString().PadLeft(4, '0'), - GateID = gates[gateIndex].Integer64ID, - GateName = gates[gateIndex].Name, + GateID = gate.Integer64ID, + GateName = gate.Name ?? string.Empty, Name = $"{airline.IATA}{flightNumber.ToString().PadLeft(4, '0')}", - SortDestinationID = sortDestinations[sortDestinationIndex].Integer64ID, - SortDestinationName = sortDestinations[sortDestinationIndex].Name, + SortDestinationID = altSortDestination is not null ? altSortDestination.Integer64ID : airline.SortDestinationID, + SortDestinationName = altSortDestination is not null ? altSortDestination.Name ?? string.Empty : airline.SortDestinationName, }); flightNumber++; - gateIndex++; - sortDestinationIndex++; departTime = departTime.Add(TimeSpan.FromMinutes(10)); - - if (gateIndex > gates.Count - 1) - { - gateIndex = 0; - } - - if (sortDestinationIndex > sortDestinations.Count - 1) - { - sortDestinationIndex = 0; - } } } } @@ -189,6 +249,14 @@ private void BuildSortDestinations() { Name = "MU4", }); + _ = SortDestinationDataLayer.CreateAsync(new SortDestination() + { + Name = "MU5", + }); + _ = SortDestinationDataLayer.CreateAsync(new SortDestination() + { + Name = "MU6", + }); } /// diff --git a/JMayer.Example.ASPReact.Server/Flights/CodeShareEqualityComparer.cs b/JMayer.Example.ASPReact.Server/Flights/CodeShareEqualityComparer.cs index e4ac96c..480e294 100644 --- a/JMayer.Example.ASPReact.Server/Flights/CodeShareEqualityComparer.cs +++ b/JMayer.Example.ASPReact.Server/Flights/CodeShareEqualityComparer.cs @@ -10,7 +10,7 @@ public class CodeShareEqualityComparer : IEqualityComparer /// public bool Equals(CodeShare? x, CodeShare? y) { - if (x == null || y == null) + if (x is null || y is null) { return false; } diff --git a/JMayer.Example.ASPReact.Server/Flights/Flight.cs b/JMayer.Example.ASPReact.Server/Flights/Flight.cs index 0d8b098..eee80b0 100644 --- a/JMayer.Example.ASPReact.Server/Flights/Flight.cs +++ b/JMayer.Example.ASPReact.Server/Flights/Flight.cs @@ -6,7 +6,7 @@ namespace JMayer.Example.ASPReact.Server.Flights; /// /// The class represents a flight in the flight schedule. /// -public class Flight : UserEditableDataObject +public class Flight : DataObject { /// /// The property gets/sets the IATA code for the airline for the flight. diff --git a/JMayer.Example.ASPReact.Server/Flights/FlightController.cs b/JMayer.Example.ASPReact.Server/Flights/FlightController.cs index a728b8e..d91aa99 100644 --- a/JMayer.Example.ASPReact.Server/Flights/FlightController.cs +++ b/JMayer.Example.ASPReact.Server/Flights/FlightController.cs @@ -1,4 +1,4 @@ -using JMayer.Web.Mvc.Controller; +using JMayer.Web.Mvc.Controller.Api; using Microsoft.AspNetCore.Mvc; namespace JMayer.Example.ASPReact.Server.Flights; @@ -8,8 +8,24 @@ namespace JMayer.Example.ASPReact.Server.Flights; /// [Route("api/[controller]")] [ApiController] -public class FlightController : UserEditableController +public class FlightController : StandardCRUDController { /// public FlightController(IFlightDataLayer dataLayer, ILogger logger) : base(dataLayer, logger) { } + + /// + /// Overridden to hide the string version of this. The client doesn't use it and swagger complains about a conflict when its exposed. + [NonAction] + public override Task DeleteAsync(string id) + { + return base.DeleteAsync(id); + } + + /// + /// Overridden to hide the string version of this. The client doesn't use it and swagger complains about a conflict when its exposed. + [NonAction] + public override Task GetSingleAsync(string id) + { + return base.GetSingleAsync(id); + } } diff --git a/JMayer.Example.ASPReact.Server/Flights/FlightDataLayer.cs b/JMayer.Example.ASPReact.Server/Flights/FlightDataLayer.cs index 2a86c59..c99c7d7 100644 --- a/JMayer.Example.ASPReact.Server/Flights/FlightDataLayer.cs +++ b/JMayer.Example.ASPReact.Server/Flights/FlightDataLayer.cs @@ -9,7 +9,7 @@ namespace JMayer.Example.ASPReact.Server.Flights; /// /// The class manages CRUD interactions with the database for a flight. /// -public class FlightDataLayer : UserEditableDataLayer, IFlightDataLayer +public class FlightDataLayer : StandardCRUDDataLayer, IFlightDataLayer { /// /// Used to access the airline data. @@ -34,6 +34,8 @@ public class FlightDataLayer : UserEditableDataLayer, IFlightDataLayer /// Used to access the sort desintation data. public FlightDataLayer(IAirlineDataLayer airlineDataLayer, IGateDataLayer gateDataLayer, ISortDestinationDataLayer sortDestinationDataLayer) { + IsOldDataObjectDetectionEnabled = true; + _airlineDataLayer = airlineDataLayer; _gateDataLayer = gateDataLayer; _sortDestinationDataLayer = sortDestinationDataLayer; @@ -62,30 +64,30 @@ public override async Task> ValidateAsync(Flight dataObje { List validationResults = await base.ValidateAsync(dataObject, cancellationToken); - if (await _airlineDataLayer.ExistAsync(obj => obj.Integer64ID == dataObject.AirlineID, cancellationToken) == false) + if (await _airlineDataLayer.ExistAsync(obj => obj.Integer64ID == dataObject.AirlineID, cancellationToken) is false) { validationResults.Add(new ValidationResult($"The {dataObject.AirlineID} airline was not found in the data store.", [nameof(Flight.AirlineID)])); } - if (await _gateDataLayer.ExistAsync(obj => obj.Integer64ID == dataObject.GateID, cancellationToken) == false) + if (await _gateDataLayer.ExistAsync(obj => obj.Integer64ID == dataObject.GateID, cancellationToken) is false) { validationResults.Add(new ValidationResult($"The {dataObject.GateID} gate was not found in the data store.", [nameof(Flight.GateID)])); } - if (await _sortDestinationDataLayer.ExistAsync(obj => obj.Integer64ID == dataObject.SortDestinationID, cancellationToken) == false) + if (await _sortDestinationDataLayer.ExistAsync(obj => obj.Integer64ID == dataObject.SortDestinationID, cancellationToken) is false) { validationResults.Add(new ValidationResult($"The {dataObject.SortDestinationID} sort destination was not found in the data store.", [nameof(Flight.SortDestinationID)])); } foreach (var codeShare in dataObject.CodeShares) { - if (await _airlineDataLayer.ExistAsync(obj => obj.Integer64ID == codeShare.AirlineID, cancellationToken) == false) + if (await _airlineDataLayer.ExistAsync(obj => obj.Integer64ID == codeShare.AirlineID, cancellationToken) is false) { validationResults.Add(new ValidationResult($"The {codeShare.AirlineID} airline for the codeshare was not found in the data store.", [nameof(CodeShare.AirlineID)])); } } - if (await ExistAsync(obj => obj.Integer64ID != dataObject.Integer64ID && obj.AirlineID == dataObject.AirlineID && obj.FlightNumber == dataObject.FlightNumber && obj.Destination == dataObject.Destination, cancellationToken) == true) + if (await ExistAsync(obj => obj.Integer64ID != dataObject.Integer64ID && obj.AirlineID == dataObject.AirlineID && obj.FlightNumber == dataObject.FlightNumber && obj.Destination == dataObject.Destination, cancellationToken) is true) { validationResults.Add(new ValidationResult("The flight already exists in the schedule.", [nameof(Flight.FlightNumber)])); } diff --git a/JMayer.Example.ASPReact.Server/Flights/FlightEqualityComparer.cs b/JMayer.Example.ASPReact.Server/Flights/FlightEqualityComparer.cs index 5473acf..6951b2f 100644 --- a/JMayer.Example.ASPReact.Server/Flights/FlightEqualityComparer.cs +++ b/JMayer.Example.ASPReact.Server/Flights/FlightEqualityComparer.cs @@ -43,7 +43,7 @@ public FlightEqualityComparer(bool excludeCreatedOn, bool excludeID, bool exclud /// public bool Equals(Flight? x, Flight? y) { - if (x == null || y == null) + if (x is null || y is null) { return false; } @@ -56,7 +56,7 @@ public bool Equals(Flight? x, Flight? y) { for (int index = 0; index < x.CodeShares.Count; index++) { - if (new CodeShareEqualityComparer().Equals(x.CodeShares[index], y.CodeShares[index]) == false) + if (new CodeShareEqualityComparer().Equals(x.CodeShares[index], y.CodeShares[index]) is false) { return false; } @@ -72,7 +72,8 @@ public bool Equals(Flight? x, Flight? y) && (_excludeID || x.Integer64ID == y.Integer64ID) && (_excludeLastEditedOn || x.LastEditedBy == y.LastEditedBy) && x.Name == y.Name - && x.Destination == y.Destination; + && x.Destination == y.Destination + && x.SortDestinationID == y.SortDestinationID; } /// diff --git a/JMayer.Example.ASPReact.Server/Flights/IFlightDataLayer.cs b/JMayer.Example.ASPReact.Server/Flights/IFlightDataLayer.cs index 826f9cf..05b371f 100644 --- a/JMayer.Example.ASPReact.Server/Flights/IFlightDataLayer.cs +++ b/JMayer.Example.ASPReact.Server/Flights/IFlightDataLayer.cs @@ -5,6 +5,6 @@ namespace JMayer.Example.ASPReact.Server.Flights; /// /// The interface for interacting with a flight collection in a database using CRUD operations. /// -public interface IFlightDataLayer : IUserEditableDataLayer +public interface IFlightDataLayer : IStandardCRUDDataLayer { } diff --git a/JMayer.Example.ASPReact.Server/Gates/Gate.cs b/JMayer.Example.ASPReact.Server/Gates/Gate.cs index e269d93..b6af321 100644 --- a/JMayer.Example.ASPReact.Server/Gates/Gate.cs +++ b/JMayer.Example.ASPReact.Server/Gates/Gate.cs @@ -1,12 +1,18 @@ using JMayer.Data.Data; +using System.ComponentModel.DataAnnotations; namespace JMayer.Example.ASPReact.Server.Gates; /// /// The class represents a gate in an airport. /// -public class Gate : UserEditableDataObject +public class Gate : DataObject { + /// + /// Overridden to add Required data annotation. + [Required] + public override string? Name { get => base.Name; set => base.Name = value; } + /// /// The default constructor. /// diff --git a/JMayer.Example.ASPReact.Server/Gates/GateController.cs b/JMayer.Example.ASPReact.Server/Gates/GateController.cs index 0040ac6..c5b5236 100644 --- a/JMayer.Example.ASPReact.Server/Gates/GateController.cs +++ b/JMayer.Example.ASPReact.Server/Gates/GateController.cs @@ -1,6 +1,5 @@ -using JMayer.Web.Mvc.Controller; +using JMayer.Web.Mvc.Controller.Api; using Microsoft.AspNetCore.Mvc; -using System.Net; namespace JMayer.Example.ASPReact.Server.Gates; @@ -9,7 +8,7 @@ namespace JMayer.Example.ASPReact.Server.Gates; /// [Route("api/[controller]")] [ApiController] -public class GateController : UserEditableController +public class GateController : StandardCRUDController { /// public GateController(IGateDataLayer dataLayer, ILogger logger) : base(dataLayer, logger) { } @@ -19,9 +18,10 @@ public GateController(IGateDataLayer dataLayer, ILogger logger) /// Overriden to prevent the creation of new gates. The example will auto generate some default gates /// and the client side will only retrieve them but not edit them. /// + [NonAction] public override Task CreateAsync([FromBody] Gate dataObject) { - return Task.FromResult((IActionResult)StatusCode((int)HttpStatusCode.MethodNotAllowed)); + return base.CreateAsync(dataObject); } /// @@ -29,9 +29,10 @@ public override Task CreateAsync([FromBody] Gate dataObject) /// Overriden to prevent the deletion of gates. The example will auto generate some default gates /// and the client side will only retrieve them but not edit them. /// - public override Task DeleteAsync(long integerID) + [NonAction] + public override Task DeleteAsync(long id) { - return Task.FromResult((IActionResult)StatusCode((int)HttpStatusCode.MethodNotAllowed)); + return base.DeleteAsync(id); } /// @@ -39,9 +40,18 @@ public override Task DeleteAsync(long integerID) /// Overriden to prevent the deletion of gates. The example will auto generate some default gates /// and the client side will only retrieve them but not edit them. /// - public override Task DeleteAsync(string stringID) + [NonAction] + public override Task DeleteAsync(string id) { - return Task.FromResult((IActionResult)StatusCode((int)HttpStatusCode.MethodNotAllowed)); + return base.DeleteAsync(id); + } + + /// + /// Overridden to hide the string version of this. The client doesn't use it and swagger complains about a conflict when its exposed. + [NonAction] + public override Task GetSingleAsync(string id) + { + return base.GetSingleAsync(id); } /// @@ -49,8 +59,9 @@ public override Task DeleteAsync(string stringID) /// Overriden to prevent the updating of gates. The example will auto generate some default gates /// and the client side will only retrieve them but not edit them. /// + [NonAction] public override Task UpdateAsync([FromBody] Gate dataObject) { - return Task.FromResult((IActionResult)StatusCode((int)HttpStatusCode.MethodNotAllowed)); + return base.UpdateAsync(dataObject); } } diff --git a/JMayer.Example.ASPReact.Server/Gates/GateDataLayer.cs b/JMayer.Example.ASPReact.Server/Gates/GateDataLayer.cs index a734527..3b0405c 100644 --- a/JMayer.Example.ASPReact.Server/Gates/GateDataLayer.cs +++ b/JMayer.Example.ASPReact.Server/Gates/GateDataLayer.cs @@ -5,6 +5,6 @@ namespace JMayer.Example.ASPReact.Server.Gates; /// /// The class manages CRUD interactions with the database for a gate. /// -public class GateDataLayer : UserEditableDataLayer, IGateDataLayer +public class GateDataLayer : StandardCRUDDataLayer, IGateDataLayer { } diff --git a/JMayer.Example.ASPReact.Server/Gates/GateEqualityComparer.cs b/JMayer.Example.ASPReact.Server/Gates/GateEqualityComparer.cs index be7e0c0..0555749 100644 --- a/JMayer.Example.ASPReact.Server/Gates/GateEqualityComparer.cs +++ b/JMayer.Example.ASPReact.Server/Gates/GateEqualityComparer.cs @@ -43,7 +43,7 @@ public GateEqualityComparer(bool excludeCreatedOn, bool excludeID, bool excludeL /// public bool Equals(Gate? x, Gate? y) { - if (x == null || y == null) + if (x is null || y is null) { return false; } diff --git a/JMayer.Example.ASPReact.Server/Gates/IGateDataLayer.cs b/JMayer.Example.ASPReact.Server/Gates/IGateDataLayer.cs index c0f9dd4..44ad6f5 100644 --- a/JMayer.Example.ASPReact.Server/Gates/IGateDataLayer.cs +++ b/JMayer.Example.ASPReact.Server/Gates/IGateDataLayer.cs @@ -5,6 +5,6 @@ namespace JMayer.Example.ASPReact.Server.Gates; /// /// The interface for interacting with a gate collection in a database using CRUD operations. /// -public interface IGateDataLayer : IUserEditableDataLayer +public interface IGateDataLayer : IStandardCRUDDataLayer { } diff --git a/JMayer.Example.ASPReact.Server/JMayer.Example.ASPReact.Server.csproj b/JMayer.Example.ASPReact.Server/JMayer.Example.ASPReact.Server.csproj index 230fc97..c8372a6 100644 --- a/JMayer.Example.ASPReact.Server/JMayer.Example.ASPReact.Server.csproj +++ b/JMayer.Example.ASPReact.Server/JMayer.Example.ASPReact.Server.csproj @@ -1,19 +1,22 @@  - net8.0 + net9.0 enable enable ..\jmayer.example.aspreact.client npm run dev https://localhost:5173 - 8.0.0 + 9.0.0 + jmayer913 + jmayer913 + https://github.com/jmayer913/JMayer-Example-ASPReact - + - 8.*-* + 9.0.11 diff --git a/JMayer.Example.ASPReact.Server/SortDestinations/ISortDestinationDataLayer.cs b/JMayer.Example.ASPReact.Server/SortDestinations/ISortDestinationDataLayer.cs index 1dc576f..862ed22 100644 --- a/JMayer.Example.ASPReact.Server/SortDestinations/ISortDestinationDataLayer.cs +++ b/JMayer.Example.ASPReact.Server/SortDestinations/ISortDestinationDataLayer.cs @@ -5,6 +5,6 @@ namespace JMayer.Example.ASPReact.Server.SortDestinations; /// /// The interface for interacting with a sort destination collection in a database using CRUD operations. /// -public interface ISortDestinationDataLayer : IUserEditableDataLayer +public interface ISortDestinationDataLayer : IStandardCRUDDataLayer { } diff --git a/JMayer.Example.ASPReact.Server/SortDestinations/SortDestination.cs b/JMayer.Example.ASPReact.Server/SortDestinations/SortDestination.cs index 91905ab..1428ef1 100644 --- a/JMayer.Example.ASPReact.Server/SortDestinations/SortDestination.cs +++ b/JMayer.Example.ASPReact.Server/SortDestinations/SortDestination.cs @@ -1,12 +1,18 @@ using JMayer.Data.Data; +using System.ComponentModel.DataAnnotations; namespace JMayer.Example.ASPReact.Server.SortDestinations; /// /// The class represents a sort destination in the baggage handling system. /// -public class SortDestination : UserEditableDataObject +public class SortDestination : DataObject { + /// + /// Overridden to add Required data annotation. + [Required] + public override string? Name { get => base.Name; set => base.Name = value; } + /// /// The default constructor. /// diff --git a/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationController.cs b/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationController.cs index 262c4c6..343ec06 100644 --- a/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationController.cs +++ b/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationController.cs @@ -1,6 +1,5 @@ -using JMayer.Web.Mvc.Controller; +using JMayer.Web.Mvc.Controller.Api; using Microsoft.AspNetCore.Mvc; -using System.Net; namespace JMayer.Example.ASPReact.Server.SortDestinations; @@ -9,7 +8,7 @@ namespace JMayer.Example.ASPReact.Server.SortDestinations; /// [Route("api/[controller]")] [ApiController] -public class SortDestinationController : UserEditableController +public class SortDestinationController : StandardCRUDController { /// public SortDestinationController(ISortDestinationDataLayer dataLayer, ILogger logger) : base(dataLayer, logger) { } @@ -19,9 +18,10 @@ public SortDestinationController(ISortDestinationDataLayer dataLayer, ILogger + [NonAction] public override Task CreateAsync([FromBody] SortDestination dataObject) { - return Task.FromResult((IActionResult)StatusCode((int)HttpStatusCode.MethodNotAllowed)); + return base.CreateAsync(dataObject); } /// @@ -29,9 +29,10 @@ public override Task CreateAsync([FromBody] SortDestination dataO /// Overriden to prevent the deletion of sort destinations. The example will auto generate some default sort destinations /// and the client side will only retrieve them but not edit them. /// - public override Task DeleteAsync(long integerID) + [NonAction] + public override Task DeleteAsync(long id) { - return Task.FromResult((IActionResult)StatusCode((int)HttpStatusCode.MethodNotAllowed)); + return base.DeleteAsync(id); } /// @@ -39,9 +40,18 @@ public override Task DeleteAsync(long integerID) /// Overriden to prevent the deletion of sort destinations. The example will auto generate some default sort destinations /// and the client side will only retrieve them but not edit them. /// - public override Task DeleteAsync(string stringID) + [NonAction] + public override Task DeleteAsync(string id) { - return Task.FromResult((IActionResult)StatusCode((int)HttpStatusCode.MethodNotAllowed)); + return base.DeleteAsync(id); + } + + /// + /// Overridden to hide the string version of this. The client doesn't use it and swagger complains about a conflict when its exposed. + [NonAction] + public override Task GetSingleAsync(string id) + { + return base.GetSingleAsync(id); } /// @@ -49,8 +59,9 @@ public override Task DeleteAsync(string stringID) /// Overriden to prevent the updating of sort destinations. The example will auto generate some default sort destinations /// and the client side will only retrieve them but not edit them. /// + [NonAction] public override Task UpdateAsync([FromBody] SortDestination dataObject) { - return Task.FromResult((IActionResult)StatusCode((int)HttpStatusCode.MethodNotAllowed)); + return base.UpdateAsync(dataObject); } } diff --git a/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationDataLayer.cs b/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationDataLayer.cs index ae697c2..f796f17 100644 --- a/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationDataLayer.cs +++ b/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationDataLayer.cs @@ -5,6 +5,6 @@ namespace JMayer.Example.ASPReact.Server.SortDestinations; /// /// The class manages CRUD interactions with the database for a sort destination. /// -public class SortDestinationDataLayer : UserEditableDataLayer, ISortDestinationDataLayer +public class SortDestinationDataLayer : StandardCRUDDataLayer, ISortDestinationDataLayer { } diff --git a/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationEqualityComparer.cs b/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationEqualityComparer.cs index c78f75f..20605db 100644 --- a/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationEqualityComparer.cs +++ b/JMayer.Example.ASPReact.Server/SortDestinations/SortDestinationEqualityComparer.cs @@ -43,7 +43,7 @@ public SortDestinationEqualityComparer(bool excludeCreatedOn, bool excludeID, bo /// public bool Equals(SortDestination? x, SortDestination? y) { - if (x == null || y == null) + if (x is null || y is null) { return false; } diff --git a/TestProject/Airlines/AirlineDataLayer.cs b/TestProject/Airlines/AirlineDataLayer.cs index 7d6814b..20df032 100644 --- a/TestProject/Airlines/AirlineDataLayer.cs +++ b/TestProject/Airlines/AirlineDataLayer.cs @@ -6,7 +6,7 @@ namespace TestProject.Airlines; /// /// The class manages CRUD interactions with a remote server for an airline. /// -internal class AirlineDataLayer : UserEditableDataLayer +internal class AirlineDataLayer : StandardCRUDDataLayer { /// public AirlineDataLayer(HttpClient httpClient) : base(httpClient) { } diff --git a/TestProject/Airlines/AirlineEqualityComparerUnitTest.cs b/TestProject/Airlines/AirlineEqualityComparerUnitTest.cs index ee63ff3..eb508f4 100644 --- a/TestProject/Airlines/AirlineEqualityComparerUnitTest.cs +++ b/TestProject/Airlines/AirlineEqualityComparerUnitTest.cs @@ -37,6 +37,11 @@ public class AirlineEqualityComparerUnitTest /// private const string NumberCode = "999"; + /// + /// The constant for the sort destination ID. + /// + private const long SortDestinationID = 2; + /// /// The method verifies equality failure when two nulls are compared. /// @@ -149,12 +154,28 @@ public void VerifyFailureOneIsNull() LastEditedOn = DateTime.Now, Name = Name, NumberCode = NumberCode, + SortDestinationID = SortDestinationID, }; Assert.False(new AirlineEqualityComparer().Equals(airline, null)); Assert.False(new AirlineEqualityComparer().Equals(null, airline)); } + /// + /// The method verifies equality failure when the SortDestinationID property is different between the two objects. + /// + [Fact] + public void VerifyFailureSortDestinationID() + { + Airline airline1 = new() + { + SortDestinationID = SortDestinationID, + }; + Airline airline2 = new(); + + Assert.False(new AirlineEqualityComparer().Equals(airline1, airline2)); + } + /// /// The method verifies equality success when two objects (different references) are compared. /// @@ -171,6 +192,7 @@ public void VerifySuccess() LastEditedOn = DateTime.Now, Name = Name, NumberCode = NumberCode, + SortDestinationID = SortDestinationID, }; Airline airline2 = new(airline1); @@ -194,6 +216,7 @@ public void VerifySuccessExcludeCreatedOn() LastEditedOn = DateTime.Now, Name = Name, NumberCode = NumberCode, + SortDestinationID = SortDestinationID, }; Airline airline2 = new(airline1) { @@ -220,6 +243,7 @@ public void VerifySuccessExcludeID() LastEditedOn = DateTime.Now, Name = Name, NumberCode = NumberCode, + SortDestinationID = SortDestinationID, }; Airline airline2 = new(airline1) { @@ -246,6 +270,7 @@ public void VerifySuccessExcludeLastEditedOn() LastEditedOn = DateTime.Now, Name = Name, NumberCode = NumberCode, + SortDestinationID = SortDestinationID, }; Airline airline2 = new(airline1) { diff --git a/TestProject/Airlines/AirlineWebRequestUnitTest.cs b/TestProject/Airlines/AirlineWebRequestUnitTest.cs index 8b3de24..9344af4 100644 --- a/TestProject/Airlines/AirlineWebRequestUnitTest.cs +++ b/TestProject/Airlines/AirlineWebRequestUnitTest.cs @@ -1,6 +1,7 @@ using JMayer.Data.Data; using JMayer.Data.HTTP.DataLayer; using JMayer.Example.ASPReact.Server.Airlines; +using JMayer.Example.ASPReact.Server.SortDestinations; using Microsoft.AspNetCore.Mvc.Testing; using System.Net; @@ -35,6 +36,11 @@ public class AirlineWebRequestUnitTest : IClassFixture private const string BadFormattedNumberCode = "9999"; + /// + /// The constant for a bad sort destination ID. + /// + private const long BadSortDestinationID = 99; + /// /// The constant for a default IATA code. /// @@ -45,12 +51,31 @@ public class AirlineWebRequestUnitTest : IClassFixture private const string DefaultICAOCode = "ZZZ"; + /// + /// The constant for the default sort destination ID. + /// + private const long DefaultSortDestinationID = 1; + /// /// The dependency injection constructor. /// /// The factory for the web application. public AirlineWebRequestUnitTest(WebApplicationFactory factory) => _factory = factory; + /// + /// The method returns the sort destination. + /// + /// The name to search for. + /// The sort destination or null if not found. + private async Task GetSortDestinationAsync(string name) + { + HttpClient httpClient = _factory.CreateClient(); + SortDestinations.SortDestinationDataLayer dataLayer = new(httpClient); + + List? sortDestinations = await dataLayer.GetAllAsync(); + return sortDestinations?.FirstOrDefault(obj => obj.Name == name); + } + /// /// The method verifies the HTTP data layer can request an airline to be created by the server and the server can successfully process the request. /// @@ -59,16 +84,24 @@ public class AirlineWebRequestUnitTest : IClassFixtureThe IATA code assigned to the airline by the IATA Organization. /// The ICAO code assigned to the airline by the International Aviation Organization. /// The number code assigned to the airline by the IATA Organization. + /// The default sort destination for the airline. /// A Task object for the async. [Theory] - [InlineData("Alaska Airlines", "Alaska", "AS", "ASA", "027")] - [InlineData("Allegiant Air", "Allegiant", "G4", "AAY", "268")] - [InlineData("Jetblue Airways", "Jetblue", "B6", "JBU", "279")] - public async Task VerifyAddAirline(string name, string description, string iata, string icao, string numberCode) + [InlineData("Alaska Airlines", "Alaska", "AS", "ASA", "027", "MU1")] + [InlineData("Allegiant Air", "Allegiant", "G4", "AAY", "268", "MU2")] + [InlineData("Jetblue Airways", "Jetblue", "B6", "JBU", "279", "MU3")] + public async Task VerifyAddAirline(string name, string description, string iata, string icao, string numberCode, string sortDestinationName) { HttpClient httpClient = _factory.CreateClient(); AirlineDataLayer dataLayer = new(httpClient); + SortDestination? sortDestination = await GetSortDestinationAsync(sortDestinationName); + + if (sortDestination is null) + { + Assert.Fail("Failed to retrieve the sort destination."); + } + Airline airline = new() { Description = description, @@ -76,6 +109,8 @@ public async Task VerifyAddAirline(string name, string description, string iata, ICAO = icao, Name = name, NumberCode = numberCode, + SortDestinationID = sortDestination.Integer64ID, + SortDestinationName = sortDestination.Name ?? string.Empty, }; OperationResult operationResult = await dataLayer.CreateAsync(airline); @@ -100,6 +135,7 @@ public async Task VerifyAddAirlineBadIATAFailure() ICAO = DefaultICAOCode, NumberCode = Airline.ZeroNumberCode, Name = "Add Bad IATA Code Test", + SortDestinationID = DefaultSortDestinationID, }); //The operation must have failed. @@ -111,13 +147,10 @@ public async Task VerifyAddAirlineBadIATAFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Equal("The IATA must be 2 alphanumeric characters; letters must be capitalized.", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.IATA), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.IATA)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.IATA)]); + Assert.Equal("The IATA must be 2 alphanumeric characters; letters must be capitalized.", operationResult.ValidationErrors[nameof(Airline.IATA)][0]); } /// @@ -136,6 +169,7 @@ public async Task VerifyAddAirlineBadICAOFailure() ICAO = BadFormattedICAOCode, NumberCode = Airline.ZeroNumberCode, Name = "Add Bad ICAO Code Test", + SortDestinationID = DefaultSortDestinationID, }); //The operation must have failed. @@ -147,13 +181,10 @@ public async Task VerifyAddAirlineBadICAOFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Equal("The ICAO must be 3 capital letters.", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.ICAO), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.ICAO)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.ICAO)]); + Assert.Equal("The ICAO must be 3 capital letters.", operationResult.ValidationErrors[nameof(Airline.ICAO)][0]); } /// @@ -172,6 +203,7 @@ public async Task VerifyAddAirlineBadNumberCodeFailure() ICAO = DefaultICAOCode, NumberCode = BadFormattedNumberCode, Name = "Add Bad Number Code Test", + SortDestinationID = DefaultSortDestinationID, }); //The operation must have failed. @@ -183,13 +215,10 @@ public async Task VerifyAddAirlineBadNumberCodeFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Equal("The number code must be 3 digits.", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.NumberCode), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.NumberCode)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.NumberCode)]); + Assert.Equal("The number code must be 3 digits.", operationResult.ValidationErrors[nameof(Airline.NumberCode)][0]); } /// @@ -208,13 +237,9 @@ public async Task VerifyAddDuplicateAirlineICAOFailure() ICAO = "ZZC", NumberCode = Airline.ZeroNumberCode, Name = "Add Duplicate ICAO Test 1", + SortDestinationID = DefaultSortDestinationID, }); - - if (!operationResult.IsSuccessStatusCode) - { - Assert.Fail("Failed to create the first airline."); - return; - } + Assert.True(operationResult.IsSuccessStatusCode, "Failed to create the first airline."); operationResult = await dataLayer.CreateAsync(new Airline() { @@ -222,6 +247,7 @@ public async Task VerifyAddDuplicateAirlineICAOFailure() ICAO = "ZZC", NumberCode = Airline.ZeroNumberCode, Name = "Add Duplicate ICAO Test New", + SortDestinationID = DefaultSortDestinationID, }); //The operation must have failed. @@ -233,13 +259,10 @@ public async Task VerifyAddDuplicateAirlineICAOFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("ICAO must be unique", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.ICAO), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.ICAO)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.ICAO)]); + Assert.Equal("The ICAO must be unique.", operationResult.ValidationErrors[nameof(Airline.ICAO)][0]); } /// @@ -258,13 +281,9 @@ public async Task VerifyAddDuplicateAirlineNameFailure() ICAO = "ZZA", NumberCode = Airline.ZeroNumberCode, Name = "Add Duplicate Name Test", + SortDestinationID = DefaultSortDestinationID, }); - - if (!operationResult.IsSuccessStatusCode) - { - Assert.Fail("Failed to create the first airline."); - return; - } + Assert.True(operationResult.IsSuccessStatusCode, "Failed to create the first airline."); operationResult = await dataLayer.CreateAsync(new Airline() { @@ -272,6 +291,7 @@ public async Task VerifyAddDuplicateAirlineNameFailure() ICAO = "ZZB", NumberCode = Airline.ZeroNumberCode, Name = "Add Duplicate Name Test", + SortDestinationID = DefaultSortDestinationID, }); //The operation must have failed. @@ -283,13 +303,10 @@ public async Task VerifyAddDuplicateAirlineNameFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("name already exists", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.Name), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.Name)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.Name)]); + Assert.Equal("The Add Duplicate Name Test name already exists in the data store.", operationResult.ValidationErrors[nameof(Airline.Name)][0]); } /// @@ -308,13 +325,9 @@ public async Task VerifyAddDuplicateAirlineNumberCodeFailure() ICAO = "ZZD", NumberCode = "999", Name = "Add Duplicate Number Code Test 1", + SortDestinationID = DefaultSortDestinationID, }); - - if (!operationResult.IsSuccessStatusCode) - { - Assert.Fail("Failed to create the first airline."); - return; - } + Assert.True(operationResult.IsSuccessStatusCode, "Failed to create the first airline."); operationResult = await dataLayer.CreateAsync(new Airline() { @@ -322,6 +335,7 @@ public async Task VerifyAddDuplicateAirlineNumberCodeFailure() ICAO = "ZZE", NumberCode = "999", Name = "Add Duplicate Number Code Test New", + SortDestinationID = DefaultSortDestinationID, }); //The operation must have failed. @@ -333,13 +347,44 @@ public async Task VerifyAddDuplicateAirlineNumberCodeFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); + //The correct error was returned. + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.NumberCode)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.NumberCode)]); + Assert.Equal("The number code must be unique unless the code is 000.", operationResult.ValidationErrors[nameof(Airline.NumberCode)][0]); + } + + /// + /// The method verifies the server will return a failure if the sort destination ID doesn't exist when adding a new airline. + /// + /// A Task object for the async. + [Fact] + public async Task VerifyAddAirlineSortDestinationNotFoundFailure() + { + HttpClient httpClient = _factory.CreateClient(); + AirlineDataLayer dataLayer = new(httpClient); + + OperationResult operationResult = await dataLayer.CreateAsync(new Airline() + { + IATA = "ZF", + ICAO = "ZZF", + NumberCode = "999", + Name = "Add Airline Sort Destination Not Found Test", + SortDestinationID = BadSortDestinationID, + }); + + //The operation must have failed. + Assert.False(operationResult.IsSuccessStatusCode, "The operation should have failed."); + + //No airline was returned. + Assert.Null(operationResult.DataObject); + + //A bad request status was returned. + Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); //The correct error was returned. - Assert.Contains("number code must be unique", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.NumberCode), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.SortDestinationID)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.SortDestinationID)]); + Assert.Equal($"The {BadSortDestinationID} sort destination was not found in the data store.", operationResult.ValidationErrors[nameof(Airline.SortDestinationID)][0]); } /// @@ -368,13 +413,14 @@ public async Task VerifyDeleteAirline() OperationResult operationResult = await dataLayer.CreateAsync(new Airline() { - IATA = "ZF", - ICAO = "ZZF", + IATA = "ZG", + ICAO = "ZZG", Name = "Delete Airline Test", NumberCode = Airline.ZeroNumberCode, + SortDestinationID = DefaultSortDestinationID, }); - if (operationResult.DataObject is Airline airline) + if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline airline) { operationResult = await dataLayer.DeleteAsync(airline); Assert.True(operationResult.IsSuccessStatusCode); @@ -445,14 +491,15 @@ public async Task VerifyGetSingleAirlineWithId() Airline airline = new() { - IATA = "ZG", - ICAO = "ZZG", + IATA = "ZH", + ICAO = "ZZH", Name = "Get Single Airline Test", NumberCode = Airline.ZeroNumberCode, + SortDestinationID = DefaultSortDestinationID, }; OperationResult operationResult = await dataLayer.CreateAsync(airline); - if (operationResult.DataObject is Airline createdAirline) + if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline createdAirline) { Airline? verifyAirline = await dataLayer.GetSingleAsync(createdAirline.Integer64ID); Assert.NotNull(verifyAirline); @@ -476,16 +523,24 @@ public async Task VerifyGetSingleAirlineWithId() /// The new ICAO code assigned to the airline by the International Aviation Organization. /// The orginal number code assigned to the airline by the IATA Organization. /// The new number code assigned to the airline by the IATA Organization. + /// The default sort destination for the airline. /// A Task object for the async. [Theory] - [InlineData("Air Canada", "Spirit Airlines", "Canada", "Spirit", "AC", "NK", "ACA", "NKS", "014", "487")] - [InlineData("British Airways", "Silver Airways", "British", "Silver", "BA", "3M", "BAW", "SIL", "125", "449")] - [InlineData("Flair Airlines", "Frontier Airlines", "Flair", "Frontier", "F8", "F9", "FLE", "FFT", "418", "422")] - public async Task VerifyUpdateAirline(string originalName, string newName, string originalDescription, string newDescription, string originalIata, string newIata, string originalIcao, string newIcao, string originalNumberCode, string newNumberCode) + [InlineData("Air Canada", "Spirit Airlines", "Canada", "Spirit", "AC", "NK", "ACA", "NKS", "014", "487", "MU2")] + [InlineData("British Airways", "Silver Airways", "British", "Silver", "BA", "3M", "BAW", "SIL", "125", "449", "MU3")] + [InlineData("Flair Airlines", "Frontier Airlines", "Flair", "Frontier", "F8", "F9", "FLE", "FFT", "418", "422", "MU4")] + public async Task VerifyUpdateAirline(string originalName, string newName, string originalDescription, string newDescription, string originalIata, string newIata, string originalIcao, string newIcao, string originalNumberCode, string newNumberCode, string sortDestinationName) { HttpClient httpClient = _factory.CreateClient(); AirlineDataLayer dataLayer = new(httpClient); + SortDestination? sortDestination = await GetSortDestinationAsync(sortDestinationName); + + if (sortDestination is null) + { + Assert.Fail("Failed to retrieve the sort destination."); + } + OperationResult operationResult = await dataLayer.CreateAsync(new Airline() { Description = originalDescription, @@ -493,6 +548,7 @@ public async Task VerifyUpdateAirline(string originalName, string newName, strin ICAO = originalIcao, Name = originalName, NumberCode = originalNumberCode, + SortDestinationID = DefaultSortDestinationID, }); if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline createdAirline) @@ -504,6 +560,8 @@ public async Task VerifyUpdateAirline(string originalName, string newName, strin ICAO = newIcao, Name = newName, NumberCode = newNumberCode, + SortDestinationID = sortDestination.Integer64ID, + SortDestinationName = sortDestination.Name ?? string.Empty, }; operationResult = await dataLayer.UpdateAsync(updatedAirline); @@ -529,13 +587,14 @@ public async Task VerifyUpdateAirlineBadIATAFailure() OperationResult operationResult = await dataLayer.CreateAsync(new Airline() { - IATA = "ZH", - ICAO = "ZZH", + IATA = "ZI", + ICAO = "ZZI", NumberCode = Airline.ZeroNumberCode, Name = "Update Bad IATA Code Test", + SortDestinationID = DefaultSortDestinationID, }); - if (operationResult.DataObject is Airline airline) + if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline airline) { airline.IATA = BadFormattedIATACode; operationResult = await dataLayer.UpdateAsync(airline); @@ -549,13 +608,10 @@ public async Task VerifyUpdateAirlineBadIATAFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Equal("The IATA must be 2 alphanumeric characters; letters must be capitalized.", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.IATA), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.IATA)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.IATA)]); + Assert.Equal("The IATA must be 2 alphanumeric characters; letters must be capitalized.", operationResult.ValidationErrors[nameof(Airline.IATA)][0]); } else { @@ -575,13 +631,14 @@ public async Task VerifyUpdateAirlineBadICAOFailure() OperationResult operationResult = await dataLayer.CreateAsync(new Airline() { - IATA = "ZI", - ICAO = "ZZI", + IATA = "ZJ", + ICAO = "ZZJ", NumberCode = Airline.ZeroNumberCode, Name = "Update Bad ICAO Code Test", + SortDestinationID = DefaultSortDestinationID, }); - if (operationResult.DataObject is Airline airline) + if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline airline) { airline.ICAO = BadFormattedICAOCode; operationResult = await dataLayer.UpdateAsync(airline); @@ -595,13 +652,10 @@ public async Task VerifyUpdateAirlineBadICAOFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Equal("The ICAO must be 3 capital letters.", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.ICAO), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.ICAO)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.ICAO)]); + Assert.Equal("The ICAO must be 3 capital letters.", operationResult.ValidationErrors[nameof(Airline.ICAO)][0]); } else { @@ -621,13 +675,14 @@ public async Task VerifyUpdateAirlineBadNumberCodeFailure() OperationResult operationResult = await dataLayer.CreateAsync(new Airline() { - IATA = "ZJ", - ICAO = "ZZJ", + IATA = "ZK", + ICAO = "ZZK", NumberCode = Airline.ZeroNumberCode, Name = "Update Bad Number Code Test", + SortDestinationID = DefaultSortDestinationID, }); - if (operationResult.DataObject is Airline airline) + if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline airline) { airline.NumberCode = BadFormattedNumberCode; operationResult = await dataLayer.UpdateAsync(airline); @@ -641,13 +696,10 @@ public async Task VerifyUpdateAirlineBadNumberCodeFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Equal("The number code must be 3 digits.", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.NumberCode), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.NumberCode)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.NumberCode)]); + Assert.Equal("The number code must be 3 digits.", operationResult.ValidationErrors[nameof(Airline.NumberCode)][0]); } else { @@ -667,13 +719,14 @@ public async Task VerifyUpdateAirlineOldDataConflict() OperationResult operationResult = await dataLayer.CreateAsync(new Airline() { - IATA = "ZK", - ICAO = "ZZK", + IATA = "ZL", + ICAO = "ZZL", Name = "Old Data Airline Test", NumberCode = Airline.ZeroNumberCode, + SortDestinationID = DefaultSortDestinationID, }); - if (operationResult.DataObject is Airline firstAirline) + if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline firstAirline) { Airline secondAirline = new(firstAirline); @@ -700,11 +753,11 @@ public async Task VerifyUpdateAirlineOldDataConflict() } /// - /// The method verifies the server will return a failure if the airline ICAO already exists when adding updating an airline. + /// The method verifies the server will return a failure if the sort destination ID doesn't exist when adding a new airline. /// /// A Task object for the async. [Fact] - public async Task VerifyUpdateDuplicateAirlineICAOFailure() + public async Task VerifyUpdateAirlineSortDestinationNotFoundFailure() { HttpClient httpClient = _factory.CreateClient(); AirlineDataLayer dataLayer = new(httpClient); @@ -713,27 +766,68 @@ public async Task VerifyUpdateDuplicateAirlineICAOFailure() { IATA = "ZM", ICAO = "ZZM", + Name = "Update Airline Sort Destination Not Found Test", NumberCode = Airline.ZeroNumberCode, - Name = "Update Duplicate ICAO Test 1", + SortDestinationID = DefaultSortDestinationID, }); - if (!operationResult.IsSuccessStatusCode) + if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline airline) { - Assert.Fail("Failed to create the first airline."); - return; + airline.SortDestinationID = BadSortDestinationID; + operationResult = await dataLayer.UpdateAsync(airline); + + //The operation must have failed. + Assert.False(operationResult.IsSuccessStatusCode, "The operation should have failed."); + + //No airline was returned. + Assert.Null(operationResult.DataObject); + + //A bad request status was returned. + Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); + + //The correct error was returned. + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.SortDestinationID)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.SortDestinationID)]); + Assert.Equal($"The {BadSortDestinationID} sort destination was not found in the data store.", operationResult.ValidationErrors[nameof(Airline.SortDestinationID)][0]); } + else + { + Assert.Fail("Failed to create the airline."); + } + } - operationResult = await dataLayer.CreateAsync(new Airline() + /// + /// The method verifies the server will return a failure if the airline ICAO already exists when adding updating an airline. + /// + /// A Task object for the async. + [Fact] + public async Task VerifyUpdateDuplicateAirlineICAOFailure() + { + HttpClient httpClient = _factory.CreateClient(); + AirlineDataLayer dataLayer = new(httpClient); + + OperationResult operationResult = await dataLayer.CreateAsync(new Airline() { IATA = "ZN", ICAO = "ZZN", NumberCode = Airline.ZeroNumberCode, + Name = "Update Duplicate ICAO Test 1", + SortDestinationID = DefaultSortDestinationID, + }); + Assert.True(operationResult.IsSuccessStatusCode, "Failed to create the first airline."); + + operationResult = await dataLayer.CreateAsync(new Airline() + { + IATA = "ZO", + ICAO = "ZZO", + NumberCode = Airline.ZeroNumberCode, Name = "Update Duplicate ICAO Test 2", + SortDestinationID = DefaultSortDestinationID, }); if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline airline) { - airline.ICAO = "ZZM"; + airline.ICAO = "ZZN"; operationResult = await dataLayer.UpdateAsync(airline); //The operation must have failed. @@ -745,13 +839,10 @@ public async Task VerifyUpdateDuplicateAirlineICAOFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("ICAO must be unique", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.ICAO), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.ICAO)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.ICAO)]); + Assert.Equal("The ICAO must be unique.", operationResult.ValidationErrors[nameof(Airline.ICAO)][0]); } else { @@ -771,24 +862,21 @@ public async Task VerifyUpdateDuplicateAirlineNameFailure() OperationResult operationResult = await dataLayer.CreateAsync(new Airline() { - IATA = "ZO", - ICAO = "ZZO", + IATA = "ZP", + ICAO = "ZZP", NumberCode = Airline.ZeroNumberCode, Name = "Update Duplicate Name Test 1", + SortDestinationID = DefaultSortDestinationID, }); - - if (!operationResult.IsSuccessStatusCode) - { - Assert.Fail("Failed to create the first airline."); - return; - } + Assert.True(operationResult.IsSuccessStatusCode, "Failed to create the first airline."); operationResult = await dataLayer.CreateAsync(new Airline() { - IATA = "ZP", - ICAO = "ZZP", + IATA = "ZQ", + ICAO = "ZZQ", NumberCode = Airline.ZeroNumberCode, Name = "Update Duplicate Name Test 2", + SortDestinationID = DefaultSortDestinationID, }); if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline airline) @@ -805,13 +893,10 @@ public async Task VerifyUpdateDuplicateAirlineNameFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("name already exists", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.Name), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.Name)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.Name)]); + Assert.Equal($"The {airline.Name} name already exists in the data store.", operationResult.ValidationErrors[nameof(Airline.Name)][0]); } else { @@ -831,24 +916,21 @@ public async Task VerifyUpdateDuplicateAirlineNumberCodeFailure() OperationResult operationResult = await dataLayer.CreateAsync(new Airline() { - IATA = "ZQ", - ICAO = "ZZQ", + IATA = "ZR", + ICAO = "ZZR", NumberCode = "997", Name = "Update Duplicate Number Code Test Test 1", + SortDestinationID = DefaultSortDestinationID, }); - - if (!operationResult.IsSuccessStatusCode) - { - Assert.Fail("Failed to create the first airline."); - return; - } + Assert.True(operationResult.IsSuccessStatusCode, "Failed to create the first airline."); operationResult = await dataLayer.CreateAsync(new Airline() { - IATA = "ZR", - ICAO = "ZZR", + IATA = "ZS", + ICAO = "ZZS", NumberCode = "998", Name = "Update Duplicate Number Code Test 2", + SortDestinationID = DefaultSortDestinationID, }); if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Airline airline) @@ -865,13 +947,10 @@ public async Task VerifyUpdateDuplicateAirlineNumberCodeFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("number code must be unique", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Airline.NumberCode), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Airline.NumberCode)); + Assert.Single(operationResult.ValidationErrors[nameof(Airline.NumberCode)]); + Assert.Equal("The number code must be unique unless the code is 000.", operationResult.ValidationErrors[nameof(Airline.NumberCode)][0]); } else { diff --git a/TestProject/Flights/FlightDataLayer.cs b/TestProject/Flights/FlightDataLayer.cs index 893e54c..62d0656 100644 --- a/TestProject/Flights/FlightDataLayer.cs +++ b/TestProject/Flights/FlightDataLayer.cs @@ -6,7 +6,7 @@ namespace TestProject.Flights; /// /// The class manages CRUD interactions with a remote server for a flight. /// -internal class FlightDataLayer : UserEditableDataLayer +internal class FlightDataLayer : StandardCRUDDataLayer { /// public FlightDataLayer(HttpClient httpClient) : base(httpClient) { } diff --git a/TestProject/Flights/FlightEqualityComparerUnitTest.cs b/TestProject/Flights/FlightEqualityComparerUnitTest.cs index 09dbe47..863e578 100644 --- a/TestProject/Flights/FlightEqualityComparerUnitTest.cs +++ b/TestProject/Flights/FlightEqualityComparerUnitTest.cs @@ -42,6 +42,11 @@ public class FlightEqualityComparerUnitTest /// private const string Name = "A Name"; + /// + /// The constant for the sort destination ID. + /// + private const long SortDestinationID = 3; + /// /// The method verifies equality failure when the AirlineID property is different between the two objects. /// @@ -202,12 +207,28 @@ public void VerifyFailureOneIsNull() Integer64ID = ID, LastEditedOn = DateTime.Now, Name = Name, + SortDestinationID = SortDestinationID, }; Assert.False(new FlightEqualityComparer().Equals(flight, null)); Assert.False(new FlightEqualityComparer().Equals(null, flight)); } + /// + /// The method verifies equality failure when the SortDestinationID property is different between the two objects. + /// + [Fact] + public void VerifyFailureSortDestinationID() + { + Flight flight1 = new() + { + SortDestinationID = SortDestinationID, + }; + Flight flight2 = new(); + + Assert.False(new FlightEqualityComparer().Equals(flight1, flight2)); + } + /// /// The method verifies equality success when two objects (different references) are compared. /// @@ -227,6 +248,7 @@ public void VerifySuccess() Integer64ID = ID, LastEditedOn = DateTime.Now, Name = Name, + SortDestinationID = SortDestinationID, }; Flight flight2 = new(flight1); @@ -253,6 +275,7 @@ public void VerifySuccessExcludeCreatedOn() Integer64ID = ID, LastEditedOn = DateTime.Now, Name = Name, + SortDestinationID = SortDestinationID, }; Flight flight2 = new(flight1) { @@ -282,6 +305,7 @@ public void VerifySuccessExcludeID() Integer64ID = ID, LastEditedOn = DateTime.Now, Name = Name, + SortDestinationID = SortDestinationID, }; Flight flight2 = new(flight1) { @@ -311,6 +335,7 @@ public void VerifySuccessExcludeLastEditedOn() Integer64ID = ID, LastEditedOn = DateTime.Now, Name = Name, + SortDestinationID = SortDestinationID, }; Flight flight2 = new(flight1) { diff --git a/TestProject/Flights/FlightWebRequestUnitTest.cs b/TestProject/Flights/FlightWebRequestUnitTest.cs index 176b707..cb5d420 100644 --- a/TestProject/Flights/FlightWebRequestUnitTest.cs +++ b/TestProject/Flights/FlightWebRequestUnitTest.cs @@ -169,6 +169,7 @@ public class FlightWebRequestUnitTest : IClassFixtureA list of CodeShare objects. private async Task> CreateCodeSharesAsync(string? codeshareCommaSeparatedList) { + if (codeshareCommaSeparatedList is null) + { + return []; + } + List codeshares = []; + string[] splitCodeShares = codeshareCommaSeparatedList.Split(',', StringSplitOptions.RemoveEmptyEntries); - if (codeshareCommaSeparatedList != null) + foreach (var codeshareString in splitCodeShares) { - string[] splitCodeShares = codeshareCommaSeparatedList.Split([','], StringSplitOptions.RemoveEmptyEntries); - - foreach (var codeshareString in splitCodeShares) - { - string airlineIATACode = codeshareString.Substring(0, 2); - string flightNumber = codeshareString.Substring(2); + string airlineIATACode = codeshareString.Substring(0, 2); + string flightNumber = codeshareString.Substring(2); - Airline? airline = await GetAirlineByIATAAsync(airlineIATACode); + Airline? airline = await GetAirlineByIATAAsync(airlineIATACode); - if (airline != null) + if (airline is not null) + { + codeshares.Add(new CodeShare() { - codeshares.Add(new CodeShare() - { - AirlineID = airline.Integer64ID, - FlightNumber = flightNumber, - }); - } + AirlineID = airline.Integer64ID, + FlightNumber = flightNumber, + }); } } @@ -219,7 +221,6 @@ private async Task> CreateCodeSharesAsync(string? codeshareComma Airlines.AirlineDataLayer dataLayer = new(httpClient); List? airlines = await dataLayer.GetAllAsync(); - return airlines?.FirstOrDefault(obj => obj.IATA == iata); } @@ -234,7 +235,6 @@ private async Task> CreateCodeSharesAsync(string? codeshareComma GateDataLyaer dataLayer = new(httpClient); List? gates = await dataLayer.GetAllAsync(); - return gates?.FirstOrDefault(obj => obj.Name == name); } @@ -249,88 +249,9 @@ private async Task> CreateCodeSharesAsync(string? codeshareComma SortDestinations.SortDestinationDataLayer dataLayer = new(httpClient); List? sortDestinations = await dataLayer.GetAllAsync(); - return sortDestinations?.FirstOrDefault(obj => obj.Name == name); } - /// - /// The method verifies the server will return a failure if the flight destination is badly formatted when adding a new flight. - /// - /// A Task object for the async. - [Fact] - public async Task VerifyAddFlightBadDestinationFailure() - { - HttpClient httpClient = _factory.CreateClient(); - FlightDataLayer dataLayer = new(httpClient); - - OperationResult operationResult = await dataLayer.CreateAsync(new Flight() - { - AirlineID = DefaultAirlineID, - DepartTime = DateTime.Now.TimeOfDay, - FlightNumber = "0000", - GateID = DefaultGateID, - Name = "Add Bad Destination Test", - Destination = BadFormatttedDestination, - SortDestinationID = DefaultSortDestinationID, - }); - - //The operation must have failed. - Assert.False(operationResult.IsSuccessStatusCode, "The operation should have failed."); - - //No airline was returned. - Assert.Null(operationResult.DataObject); - - //A bad request status was returned. - Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - - //The correct error was returned. - Assert.Equal("The city must be 3 capital letters.", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.Destination), operationResult.ServerSideValidationResult.Errors[0].PropertyName); - } - - /// - /// The method verifies the server will return a failure if the flight number is badly formatted when adding a new flight. - /// - /// A Task object for the async. - [Fact] - public async Task VerifyAddFlightBadFlightNumberFailure() - { - HttpClient httpClient = _factory.CreateClient(); - FlightDataLayer dataLayer = new(httpClient); - - OperationResult operationResult = await dataLayer.CreateAsync(new Flight() - { - AirlineID = DefaultAirlineID, - DepartTime = DateTime.Now.TimeOfDay, - FlightNumber = BadFormatttedFlightNumber, - GateID = DefaultGateID, - Name = "Add Bad Flight Number Test", - Destination = DefaultAirportCode, - SortDestinationID = DefaultSortDestinationID, - }); - - //The operation must have failed. - Assert.False(operationResult.IsSuccessStatusCode, "The operation should have failed."); - - //No airline was returned. - Assert.Null(operationResult.DataObject); - - //A bad request status was returned. - Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - - //The correct error was returned. - Assert.Equal("The flight number must be 4 digits or 4 digits and a capital letter.", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.FlightNumber), operationResult.ServerSideValidationResult.Errors[0].PropertyName); - } - /// /// The method verifies the server will return a failure if the flight already exists when adding a new flight. /// @@ -351,12 +272,7 @@ public async Task VerifyAddDuplicateFlightFailure() Destination = DefaultAirportCode, SortDestinationID = DefaultSortDestinationID, }); - - if (!operationResult.IsSuccessStatusCode) - { - Assert.Fail("Failed to create the first flight."); - return; - } + Assert.True(operationResult.IsSuccessStatusCode, "Failed to create the first flight."); operationResult = await dataLayer.CreateAsync(new Flight() { @@ -378,13 +294,10 @@ public async Task VerifyAddDuplicateFlightFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("flight already exists", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.FlightNumber), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.FlightNumber)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.FlightNumber)]); + Assert.Equal("The flight already exists in the schedule.", operationResult.ValidationErrors[nameof(Flight.FlightNumber)][0]); } /// @@ -408,26 +321,23 @@ public async Task VerifyAddFlight(string gateName, string airlineIATA, string fl Gate? gate = await GetGateAsync(gateName); - if (gate == null) + if (gate is null) { Assert.Fail("Failed to retrieve the gate."); - return; } Airline? airline = await GetAirlineByIATAAsync(airlineIATA); - if (airline == null) + if (airline is null) { Assert.Fail("Failed to retrieve the airline."); - return; } SortDestination? sortDestination = await GetSortDestinationAsync(sortDestinationName); - if (sortDestination == null) + if (sortDestination is null) { Assert.Fail("Failed to retrieve the sort destination."); - return; } List codeshares = await CreateCodeSharesAsync(codeshareCommaSeparatedList); @@ -455,7 +365,7 @@ public async Task VerifyAddFlight(string gateName, string airlineIATA, string fl /// /// A Task object for the async. [Fact] - public async Task VerifyAddFlightFailureAirlineNotFound() + public async Task VerifyAddFlightAirlineNotFoundFailure() { HttpClient httpClient = _factory.CreateClient(); FlightDataLayer dataLayer = new(httpClient); @@ -480,13 +390,82 @@ public async Task VerifyAddFlightFailureAirlineNotFound() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); + //The correct error was returned. + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.AirlineID)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.AirlineID)]); + Assert.Equal($"The {BadAirlineID} airline was not found in the data store.", operationResult.ValidationErrors[nameof(Flight.AirlineID)][0]); + } + + /// + /// The method verifies the server will return a failure if the flight destination is badly formatted when adding a new flight. + /// + /// A Task object for the async. + [Fact] + public async Task VerifyAddFlightBadDestinationFailure() + { + HttpClient httpClient = _factory.CreateClient(); + FlightDataLayer dataLayer = new(httpClient); + + OperationResult operationResult = await dataLayer.CreateAsync(new Flight() + { + AirlineID = DefaultAirlineID, + DepartTime = DateTime.Now.TimeOfDay, + FlightNumber = "0000", + GateID = DefaultGateID, + Name = "Add Bad Destination Test", + Destination = BadFormatttedDestination, + SortDestinationID = DefaultSortDestinationID, + }); + + //The operation must have failed. + Assert.False(operationResult.IsSuccessStatusCode, "The operation should have failed."); + + //No airline was returned. + Assert.Null(operationResult.DataObject); + + //A bad request status was returned. + Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); + + //The correct error was returned. + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.Destination)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.Destination)]); + Assert.Equal("The city must be 3 capital letters.", operationResult.ValidationErrors[nameof(Flight.Destination)][0]); + } + + /// + /// The method verifies the server will return a failure if the flight number is badly formatted when adding a new flight. + /// + /// A Task object for the async. + [Fact] + public async Task VerifyAddFlightBadFlightNumberFailure() + { + HttpClient httpClient = _factory.CreateClient(); + FlightDataLayer dataLayer = new(httpClient); + + OperationResult operationResult = await dataLayer.CreateAsync(new Flight() + { + AirlineID = DefaultAirlineID, + DepartTime = DateTime.Now.TimeOfDay, + FlightNumber = BadFormatttedFlightNumber, + GateID = DefaultGateID, + Name = "Add Bad Flight Number Test", + Destination = DefaultAirportCode, + SortDestinationID = DefaultSortDestinationID, + }); + + //The operation must have failed. + Assert.False(operationResult.IsSuccessStatusCode, "The operation should have failed."); + + //No airline was returned. + Assert.Null(operationResult.DataObject); + + //A bad request status was returned. + Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); //The correct error was returned. - Assert.Contains("airline was not found", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.AirlineID), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.FlightNumber)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.FlightNumber)]); + Assert.Equal("The flight number must be 4 digits or 4 digits and a capital letter.", operationResult.ValidationErrors[nameof(Flight.FlightNumber)][0]); } /// @@ -494,7 +473,7 @@ public async Task VerifyAddFlightFailureAirlineNotFound() /// /// A Task object for the async. [Fact] - public async Task VerifyAddFlightFailureCodeShareAirlineNotFound() + public async Task VerifyAddFlightCodeShareAirlineNotFoundFailure() { HttpClient httpClient = _factory.CreateClient(); FlightDataLayer dataLayer = new(httpClient); @@ -520,13 +499,10 @@ public async Task VerifyAddFlightFailureCodeShareAirlineNotFound() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("airline for the codeshare was not found", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(CodeShare.AirlineID), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.AirlineID)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.AirlineID)]); + Assert.Equal($"The {BadAirlineID} airline for the codeshare was not found in the data store.", operationResult.ValidationErrors[nameof(Flight.AirlineID)][0]); } /// @@ -534,7 +510,7 @@ public async Task VerifyAddFlightFailureCodeShareAirlineNotFound() /// /// A Task object for the async. [Fact] - public async Task VerifyAddFlightFailureGateNotFound() + public async Task VerifyAddFlightGateNotFoundFailure() { HttpClient httpClient = _factory.CreateClient(); FlightDataLayer dataLayer = new(httpClient); @@ -559,13 +535,10 @@ public async Task VerifyAddFlightFailureGateNotFound() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("gate was not found", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.GateID), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.GateID)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.GateID)]); + Assert.Equal($"The {BadGateID} gate was not found in the data store.", operationResult.ValidationErrors[nameof(Flight.GateID)][0]); } /// @@ -573,7 +546,7 @@ public async Task VerifyAddFlightFailureGateNotFound() /// /// A Task object for the async. [Fact] - public async Task VerifyAddFlightFailureSortDestinationNotFound() + public async Task VerifyAddFlightSortDestinationNotFoundFailure() { HttpClient httpClient = _factory.CreateClient(); FlightDataLayer dataLayer = new(httpClient); @@ -598,13 +571,10 @@ public async Task VerifyAddFlightFailureSortDestinationNotFound() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("sort destination was not found", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.SortDestinationID), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.SortDestinationID)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.SortDestinationID)]); + Assert.Equal($"The {BadSortDestinationID} sort destination was not found in the data store.", operationResult.ValidationErrors[nameof(Flight.SortDestinationID)][0]); } /// @@ -633,10 +603,9 @@ public async Task VerifyDeleteAirlineCascade() Airline? airline = await CreateAirlineAsync(AirlineCascadeDelete); - if (airline == null) + if (airline is null) { Assert.Fail("Failed to create the airline."); - return; } OperationResult operationResult = await dataLayer.CreateAsync(new Flight() @@ -649,26 +618,20 @@ public async Task VerifyDeleteAirlineCascade() Destination = DefaultAirportCode, SortDestinationID = DefaultSortDestinationID, }); + Assert.True(operationResult.IsSuccessStatusCode, "Failed to create the flight."); - if (!operationResult.IsSuccessStatusCode) - { - Assert.Fail("Failed to create the flight."); - return; - } - - await new Airlines.AirlineDataLayer(httpClient).DeleteAsync(airline); + operationResult = await new Airlines.AirlineDataLayer(httpClient).DeleteAsync(airline); + Assert.True(operationResult.IsSuccessStatusCode, "Failed to delete the airline."); List? flights = await dataLayer.GetAllAsync(); - if (flights == null) + if (flights is null) { Assert.Fail("Failed to query the flights."); } - else - { - flights = flights.Where(obj => obj.AirlineID == airline.Integer64ID).ToList(); - Assert.Empty(flights); - } + + flights = [.. flights.Where(obj => obj.AirlineID == airline.Integer64ID)]; + Assert.Empty(flights); } /// @@ -692,7 +655,7 @@ public async Task VerifyDeleteFlight() SortDestinationID = DefaultSortDestinationID, }); - if (operationResult.DataObject is Flight flight) + if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Flight flight) { operationResult = await dataLayer.DeleteAsync(flight); Assert.True(operationResult.IsSuccessStatusCode); @@ -712,7 +675,6 @@ public async Task VerifyGetAllFlights() { HttpClient httpClient = _factory.CreateClient(); FlightDataLayer dataLayer = new(httpClient); - List? flights = await dataLayer.GetAllAsync(); //Flights must have been returned. @@ -729,7 +691,6 @@ public async Task VerifyGetAllListViewFlights() { HttpClient httpClient = _factory.CreateClient(); FlightDataLayer dataLayer = new(httpClient); - List? flights = await dataLayer.GetAllListViewAsync(); //List view flights must have been returned. @@ -785,12 +746,7 @@ public async Task VerifyUpdateDuplicateFlightFailure() Destination = DefaultAirportCode, SortDestinationID = DefaultSortDestinationID, }); - - if (!operationResult.IsSuccessStatusCode) - { - Assert.Fail("Failed to create the first flight."); - return; - } + Assert.True(operationResult.IsSuccessStatusCode, "Failed to create the first flight."); operationResult = await dataLayer.CreateAsync(new Flight() { @@ -826,13 +782,10 @@ public async Task VerifyUpdateDuplicateFlightFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("flight already exists", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.FlightNumber), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.FlightNumber)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.FlightNumber)]); + Assert.Equal("The flight already exists in the schedule.", operationResult.ValidationErrors[nameof(Flight.FlightNumber)][0]); } else { @@ -861,26 +814,23 @@ public async Task VerifyUpdateFlight(string gateName, string airlineIATA, string Gate? gate = await GetGateAsync(gateName); - if (gate == null) + if (gate is null) { Assert.Fail("Failed to retrieve the gate."); - return; } Airline? airline = await GetAirlineByIATAAsync(airlineIATA); - if (airline == null) + if (airline is null) { Assert.Fail("Failed to retrieve the airline."); - return; } SortDestination? sortDestination = await GetSortDestinationAsync(sortDestinationName); - if (sortDestination == null) + if (sortDestination is null) { Assert.Fail("Failed to retrieve the sort destination."); - return; } OperationResult operationResult = await dataLayer.CreateAsync(new Flight() @@ -966,13 +916,10 @@ public async Task VerifyUpdateFlightAirlineNotFoundFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("airline was not found", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.AirlineID), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.AirlineID)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.AirlineID)]); + Assert.Equal($"The {BadAirlineID} airline was not found in the data store.", operationResult.ValidationErrors[nameof(Flight.AirlineID)][0]); } else { @@ -1001,7 +948,7 @@ public async Task VerifyUpdateFlightBadDestinationFailure() SortDestinationID = DefaultSortDestinationID, }); - if (operationResult.DataObject is Flight flight) + if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Flight flight) { flight.Destination = BadFormatttedDestination; operationResult = await dataLayer.UpdateAsync(flight); @@ -1015,13 +962,10 @@ public async Task VerifyUpdateFlightBadDestinationFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Equal("The city must be 3 capital letters.", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.Destination), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.Destination)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.Destination)]); + Assert.Equal("The city must be 3 capital letters.", operationResult.ValidationErrors[nameof(Flight.Destination)][0]); } else { @@ -1050,7 +994,7 @@ public async Task VerifyUpdateFlightBadFlightNumberFailure() SortDestinationID = DefaultSortDestinationID, }); - if (operationResult.DataObject is Flight flight) + if (operationResult.IsSuccessStatusCode && operationResult.DataObject is Flight flight) { flight.FlightNumber = BadFormatttedFlightNumber; operationResult = await dataLayer.UpdateAsync(flight); @@ -1064,13 +1008,10 @@ public async Task VerifyUpdateFlightBadFlightNumberFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Equal("The flight number must be 4 digits or 4 digits and a capital letter.", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.FlightNumber), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.FlightNumber)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.FlightNumber)]); + Assert.Equal("The flight number must be 4 digits or 4 digits and a capital letter.", operationResult.ValidationErrors[nameof(Flight.FlightNumber)][0]); } else { @@ -1123,13 +1064,10 @@ public async Task VerifyUpdateFlightCodeShareAirlineNotFoundFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("airline for the codeshare was not found", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(CodeShare.AirlineID), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.AirlineID)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.AirlineID)]); + Assert.Equal($"The {BadAirlineID} airline for the codeshare was not found in the data store.", operationResult.ValidationErrors[nameof(Flight.AirlineID)][0]); } else { @@ -1181,13 +1119,10 @@ public async Task VerifyUpdateFlightGateNotFoundFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("gate was not found", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.GateID), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.GateID)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.GateID)]); + Assert.Equal($"The {BadGateID} gate was not found in the data store.", operationResult.ValidationErrors[nameof(Flight.GateID)][0]); } else { @@ -1239,13 +1174,10 @@ public async Task VerifyUpdateFlightSortDestinationNotFoundFailure() //A bad request status was returned. Assert.Equal(HttpStatusCode.BadRequest, operationResult.StatusCode); - //A validation error was returned. - Assert.NotNull(operationResult.ServerSideValidationResult); - Assert.Single(operationResult.ServerSideValidationResult.Errors); - //The correct error was returned. - Assert.Contains("sort destination was not found", operationResult.ServerSideValidationResult.Errors[0].ErrorMessage); - Assert.Equal(nameof(Flight.SortDestinationID), operationResult.ServerSideValidationResult.Errors[0].PropertyName); + Assert.Contains(operationResult.ValidationErrors, obj => obj.Key == nameof(Flight.SortDestinationID)); + Assert.Single(operationResult.ValidationErrors[nameof(Flight.SortDestinationID)]); + Assert.Equal($"The {BadSortDestinationID} sort destination was not found in the data store.", operationResult.ValidationErrors[nameof(Flight.SortDestinationID)][0]); } else { diff --git a/TestProject/Gates/GateDataLyaer.cs b/TestProject/Gates/GateDataLyaer.cs index 7dc342d..5deab23 100644 --- a/TestProject/Gates/GateDataLyaer.cs +++ b/TestProject/Gates/GateDataLyaer.cs @@ -6,7 +6,7 @@ namespace TestProject.Gates; /// /// The class manages CRUD interactions with a remote server for an airline. /// -internal class GateDataLyaer : UserEditableDataLayer +internal class GateDataLyaer : StandardCRUDDataLayer { /// public GateDataLyaer(HttpClient httpClient) : base(httpClient) { } diff --git a/TestProject/Gates/GateWebRequestUnitTest.cs b/TestProject/Gates/GateWebRequestUnitTest.cs index bed4063..4092271 100644 --- a/TestProject/Gates/GateWebRequestUnitTest.cs +++ b/TestProject/Gates/GateWebRequestUnitTest.cs @@ -47,7 +47,7 @@ public async Task VerifyAddGateNotAllowed() //No gate or server side validation result was returned. Assert.Null(operationResult.DataObject); - Assert.Null(operationResult.ServerSideValidationResult); + Assert.Empty(operationResult.ValidationErrors); //A method not allowed status was returned. Assert.Equal(HttpStatusCode.MethodNotAllowed, operationResult.StatusCode); @@ -83,7 +83,7 @@ public async Task VerifyDeleteGateNotAllowed() //No gate or server side validation result was returned. Assert.Null(operationResult.DataObject); - Assert.Null(operationResult.ServerSideValidationResult); + Assert.Empty(operationResult.ValidationErrors); //A method not allowed status was returned. Assert.Equal(HttpStatusCode.MethodNotAllowed, operationResult.StatusCode); @@ -167,7 +167,7 @@ public async Task VerifyUpdateGateNotAllowed() //No gate or server side validation result was returned. Assert.Null(operationResult.DataObject); - Assert.Null(operationResult.ServerSideValidationResult); + Assert.Empty(operationResult.ValidationErrors); //A method not allowed status was returned. Assert.Equal(HttpStatusCode.MethodNotAllowed, operationResult.StatusCode); diff --git a/TestProject/SortDestinations/SortDestinationDataLayer.cs b/TestProject/SortDestinations/SortDestinationDataLayer.cs index 3f93ce7..6269f00 100644 --- a/TestProject/SortDestinations/SortDestinationDataLayer.cs +++ b/TestProject/SortDestinations/SortDestinationDataLayer.cs @@ -6,7 +6,7 @@ namespace TestProject.SortDestinations; /// /// The class manages CRUD interactions with a remote server for a sort destination. /// -internal class SortDestinationDataLayer : UserEditableDataLayer +internal class SortDestinationDataLayer : StandardCRUDDataLayer { /// public SortDestinationDataLayer(HttpClient httpClient) : base(httpClient) { } diff --git a/TestProject/SortDestinations/SortDestinationWebRequestUnitTest.cs b/TestProject/SortDestinations/SortDestinationWebRequestUnitTest.cs index a884a7d..38838b8 100644 --- a/TestProject/SortDestinations/SortDestinationWebRequestUnitTest.cs +++ b/TestProject/SortDestinations/SortDestinationWebRequestUnitTest.cs @@ -47,7 +47,7 @@ public async Task VerifyAddSortDestinationNotAllowed() //No sort destination or server side validation result was returned. Assert.Null(operationResult.DataObject); - Assert.Null(operationResult.ServerSideValidationResult); + Assert.Empty(operationResult.ValidationErrors); //A method not allowed status was returned. Assert.Equal(HttpStatusCode.MethodNotAllowed, operationResult.StatusCode); @@ -83,7 +83,7 @@ public async Task VerifyDeleteSortDestinationNotAllowed() //No sort destination or server side validation result was returned. Assert.Null(operationResult.DataObject); - Assert.Null(operationResult.ServerSideValidationResult); + Assert.Empty(operationResult.ValidationErrors); //A method not allowed status was returned. Assert.Equal(HttpStatusCode.MethodNotAllowed, operationResult.StatusCode); @@ -167,7 +167,7 @@ public async Task VerifyUpdateSortDestinationNotAllowed() //No sort destination or server side validation result was returned. Assert.Null(operationResult.DataObject); - Assert.Null(operationResult.ServerSideValidationResult); + Assert.Empty(operationResult.ValidationErrors); //A method not allowed status was returned. Assert.Equal(HttpStatusCode.MethodNotAllowed, operationResult.StatusCode); diff --git a/TestProject/TestProject.csproj b/TestProject/TestProject.csproj index 12b16c7..2ffc64f 100644 --- a/TestProject/TestProject.csproj +++ b/TestProject/TestProject.csproj @@ -1,13 +1,13 @@ - net8.0 + net9.0 enable enable false true - 8.0.0 + 9.0.0 @@ -15,7 +15,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/jmayer.example.aspreact.client/package-lock.json b/jmayer.example.aspreact.client/package-lock.json index d49d563..e92703b 100644 --- a/jmayer.example.aspreact.client/package-lock.json +++ b/jmayer.example.aspreact.client/package-lock.json @@ -1,12 +1,12 @@ { "name": "jmayer.example.aspreact.client", - "version": "0.0.0", + "version": "9.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jmayer.example.aspreact.client", - "version": "0.0.0", + "version": "9.0.0", "dependencies": { "globals": "^16.2.0", "primeflex": "^4.0.0", @@ -716,12 +716,12 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -730,21 +730,21 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", - "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.15" @@ -789,9 +789,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", - "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -801,21 +801,21 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -1227,17 +1227,17 @@ "dev": true }, "node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", - "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "peerDependencies": { "@types/react": "^19.2.0" @@ -1677,9 +1677,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, "node_modules/data-view-buffer": { "version": "1.0.2", @@ -2060,24 +2060,23 @@ } }, "node_modules/eslint": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", - "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.4.0", - "@eslint/core": "^0.16.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.37.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -2479,9 +2478,9 @@ } }, "node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "engines": { "node": ">=18" }, @@ -3582,9 +3581,9 @@ } }, "node_modules/react-router": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", - "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -3603,11 +3602,11 @@ } }, "node_modules/react-router-dom": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", - "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz", + "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==", "dependencies": { - "react-router": "7.9.4" + "react-router": "7.9.6" }, "engines": { "node": ">=20.0.0" @@ -4257,9 +4256,9 @@ } }, "node_modules/vite": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.0.tgz", - "integrity": "sha512-oLnWs9Hak/LOlKjeSpOwD6JMks8BeICEdYMJBf6P4Lac/pO9tKiv/XhXnAM7nNfSkZahjlCZu9sS50zL8fSnsw==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/jmayer.example.aspreact.client/package.json b/jmayer.example.aspreact.client/package.json index 97d47fd..9e63522 100644 --- a/jmayer.example.aspreact.client/package.json +++ b/jmayer.example.aspreact.client/package.json @@ -1,7 +1,7 @@ { "name": "jmayer.example.aspreact.client", "private": true, - "version": "0.0.0", + "version": "9.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/jmayer.example.aspreact.client/src/components/airline/AirlineAddEditDialog.jsx b/jmayer.example.aspreact.client/src/components/airline/AirlineAddEditDialog.jsx index e1c3f4e..7ea8b9d 100644 --- a/jmayer.example.aspreact.client/src/components/airline/AirlineAddEditDialog.jsx +++ b/jmayer.example.aspreact.client/src/components/airline/AirlineAddEditDialog.jsx @@ -1,8 +1,10 @@ import React, { useState, useEffect } from 'react'; import { Button } from 'primereact/button'; import { Dialog } from 'primereact/dialog'; +import { Dropdown } from 'primereact/dropdown'; import { InputText } from 'primereact/inputtext'; import { useAirlineDataLayer } from '../../datalayers/AirlineDataLayer.jsx'; +import { useSortDestinationDataLayer } from '../../datalayers/SortDestinationDataLayer.jsx'; //The function returns the dialog for adding or updating an airline. //@param {object} props The properties accepted by the component. @@ -12,27 +14,35 @@ import { useAirlineDataLayer } from '../../datalayers/AirlineDataLayer.jsx'; //@param {bool} props.visible Used to control if the dialog is visible or not. //@param {function} props.hide Used to hide the dialog. export default function AirlineAddEditDialog({ newRecord, airline, setAirline, visible, hide }) { + const { addAirline, addAirlineValidationProblemDetails, addAirlineSuccess, updateAirline, updateAirlineValidationProblemDetails, updateAirlineSuccess } = useAirlineDataLayer(); + const { sortDestinations, getSortDestinations } = useSortDestinationDataLayer(); const [iataValidationError, setIataValidationError] = useState(''); const [icaoValidationError, setIcaoValidationError] = useState(''); const [nameValidationError, setNameValidationError] = useState(''); const [numberCodeValidationError, setNumberCodeValidationError] = useState(''); - const { addAirline, addAirlineServerSideResult, addAirlineSuccess, updateAirline, updateAirlineServerSideResult, updateAirlineSuccess } = useAirlineDataLayer(); + const [sortDestinationValidationError, setSortDestinationValidationError] = useState(''); + //Load the destinations when the component mounts. + useEffect(() => { + getSortDestinations(); + }, []); + + //Handle state changes based on add/update operations. useEffect(() => { //Hide the dialog on a successful add or update. if (addAirlineSuccess || updateAirlineSuccess) { hide(); } - //Handle displays server side errors. - if (addAirlineServerSideResult !== null) { - processServerSideValidationResult(addAirlineServerSideResult); + //Handle displayings server side errors. + if (addAirlineValidationProblemDetails !== null) { + processValidationProblemDetails(addAirlineValidationProblemDetails); } - else if (updateAirlineServerSideResult !== null) { - processServerSideValidationResult(updateAirlineServerSideResult); + else if (updateAirlineValidationProblemDetails !== null) { + processValidationProblemDetails(updateAirlineValidationProblemDetails); } - }, [addAirlineServerSideResult, addAirlineSuccess, updateAirlineServerSideResult, updateAirlineSuccess]); + }, [addAirlineValidationProblemDetails, addAirlineSuccess, updateAirlineValidationProblemDetails, updateAirlineSuccess]); //The function clears the validation and closes the dialog. const closeDialog = () => { @@ -40,6 +50,7 @@ export default function AirlineAddEditDialog({ newRecord, airline, setAirline, v setIcaoValidationError(''); setNameValidationError(''); setNumberCodeValidationError(''); + setSortDestinationValidationError(''); hide(); }; @@ -49,26 +60,30 @@ export default function AirlineAddEditDialog({ newRecord, airline, setAirline, v const icoaPass = validateICOA(); const namePass = validateName(); const numberCodePass = validateNumberCode(); + const sortDestinationPass = validateSortDestination(); - return iataPass && icoaPass && namePass && numberCodePass; + return iataPass && icoaPass && namePass && numberCodePass && sortDestinationPass; }; - //The function processes the server side validation result and sets any validation errors. - //@param {object} serverSideValidationResult What the server found wrong with the user input. - const processServerSideValidationResult = (serverSideValidationResult) => { - for (const error of serverSideValidationResult.errors) { - switch (error.propertyName) { + //The function processes the validation problem details returned by the server. + //@param {object} result The validation problem details returned by the server. + const processValidationProblemDetails = (details) => { + for (const key in details.errors) { + switch (key) { case 'Name': - setNameValidationError(error.errorMessage); + setNameValidationError(details.errors[key][0]); break; case 'IATA': - setIataValidationError(error.errorMessage); + setIataValidationError(details.errors[key][0]); break; case 'ICAO': - setIcaoValidationError(error.errorMessage); + setIcaoValidationError(details.errors[key][0]); break; case 'NumberCode': - setNumberCodeValidationError(error.errorMessage); + setNumberCodeValidationError(details.errors[key][0]); + break; + case 'SortDestinationID': + setSortDestinationValidationError(details.errors[key][0]); break; } } @@ -119,6 +134,16 @@ export default function AirlineAddEditDialog({ newRecord, airline, setAirline, v }); }; + //The function updates the sort destination field with the value selected by the user. + //@param {sortDestination} sortDestination The sort desstination the user selected. + const setSortDestination = (sortDestination) => { + setAirline({ + ...airline, + sortDestinationID: sortDestination.integer64ID, + sortDestinationName: sortDestination.name + }); + } + //The function returns if the airline's IATA field passed validation. const validateIATA = () => { const pattern = /^[A-Z0-9]{2}$/; @@ -183,6 +208,19 @@ export default function AirlineAddEditDialog({ newRecord, airline, setAirline, v return !error; }; + //The function returns if the airline's sort destination field passed validation. + const validateSortDestination = () => { + let error = ''; + + if (airline.sortDestinationID === 0) { + error = 'The sort destination is required.'; + } + + setSortDestinationValidationError(error); + + return !error; + }; + //The footer for the dialog. const footer = ( @@ -218,6 +256,11 @@ export default function AirlineAddEditDialog({ newRecord, airline, setAirline, v validateNumberCode()} onChange={(e) => setNumberCode(e.target.value)} /> {numberCodeValidationError && {numberCodeValidationError}} +
+ + element.integer64ID == airline.sortDestinationID)} options={sortDestinations} optionLabel="name" filter placeholder="Select a default sort destination for the airline's baggage" onBlur={(e) => validateSortDestination()} onChange={(e) => setSortDestination(e.value)} /> + {sortDestinationValidationError && {sortDestinationValidationError}} +
); diff --git a/jmayer.example.aspreact.client/src/components/airline/AirlinePage.jsx b/jmayer.example.aspreact.client/src/components/airline/AirlinePage.jsx index f72ea5e..7ecf1d1 100644 --- a/jmayer.example.aspreact.client/src/components/airline/AirlinePage.jsx +++ b/jmayer.example.aspreact.client/src/components/airline/AirlinePage.jsx @@ -86,6 +86,7 @@ export default function AirlinePage() { + diff --git a/jmayer.example.aspreact.client/src/components/flightSchedule/FlightAddEditDialog.jsx b/jmayer.example.aspreact.client/src/components/flightSchedule/FlightAddEditDialog.jsx index c7241bb..21e7e5b 100644 --- a/jmayer.example.aspreact.client/src/components/flightSchedule/FlightAddEditDialog.jsx +++ b/jmayer.example.aspreact.client/src/components/flightSchedule/FlightAddEditDialog.jsx @@ -17,7 +17,7 @@ import { useSortDestinationDataLayer } from '../../datalayers/SortDestinationDat //@param {function} props.hide Used to hide the dialog. export default function FlightAddEditDialog({ newRecord, flight, setFlight, visible, hide }) { const { airlines, getAirlines } = useAirlineDataLayer(); - const { addFlight, addFlightServerSideResult, addFlightSuccess, updateFlight, updateFlightServerSideResult, updateFlightSuccess } = useFlightDataLayer(); + const { addFlight, addFlightValidationProblemDetails, addFlightSuccess, updateFlight, updateFlightValidationProblemDetails, updateFlightSuccess } = useFlightDataLayer(); const { gates, getGates } = useGateDataLayer(); const { sortDestinations, getSortDestinations } = useSortDestinationDataLayer(); const [airlineValidationError, setAirlineValidationError] = useState(''); @@ -51,15 +51,15 @@ export default function FlightAddEditDialog({ newRecord, flight, setFlight, visi hide(); } - //Handle displays server side errors. - if (addFlightServerSideResult !== null) { - processServerSideValidationResult(addFlightServerSideResult); + //Handle displayings server side errors. + if (addFlightValidationProblemDetails !== null) { + processValidationProblemDetails(addFlightValidationProblemDetails); } - else if (updateFlightServerSideResult !== null) { - processServerSideValidationResult(updateFlightServerSideResult); + else if (updateFlightValidationProblemDetails !== null) { + processValidationProblemDetails(updateFlightValidationProblemDetails); } - }, [addFlightServerSideResult, addFlightSuccess, updateFlightServerSideResult, updateFlightSuccess]); + }, [addFlightValidationProblemDetails, addFlightSuccess, updateFlightValidationProblemDetails, updateFlightSuccess]); //The function clears the validation and closes the dialog. const closeDialog = () => { @@ -82,16 +82,16 @@ export default function FlightAddEditDialog({ newRecord, flight, setFlight, visi return airlinePass && destinationPass && gatePass && flightNumberPass && sortDestinaitonPass; }; - //The function processes the server side validation result and sets any validation errors. - //@param {object} serverSideValidationResult What the server found wrong with the user input. - const processServerSideValidationResult = (serverSideValidationResult) => { - for (const error of serverSideValidationResult.errors) { - switch (error.propertyName) { + //The function processes the validation problem details returned by the server. + //@param {object} result The validation problem details returned by the server. + const processValidationProblemDetails = (details) => { + for (const key in details.errors) { + switch (key) { case 'Destination': - setDestinationValidationError(error.errorMessage); + setDestinationValidationError(details.errors[key][0]); break; case 'FlightNumber': - setFlightNumberValidationError(error.errorMessage); + setFlightNumberValidationError(details.errors[key][0]); break; } } diff --git a/jmayer.example.aspreact.client/src/datalayers/AirlineDataLayer.jsx b/jmayer.example.aspreact.client/src/datalayers/AirlineDataLayer.jsx index 46763a4..08aef90 100644 --- a/jmayer.example.aspreact.client/src/datalayers/AirlineDataLayer.jsx +++ b/jmayer.example.aspreact.client/src/datalayers/AirlineDataLayer.jsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { useError } from '../components/errorDialog/ErrorProvider.jsx'; -import { shapeValidationResult } from './DataLayerHelper.jsx'; //Defines the initial airline object. const initialAirline = { @@ -9,6 +8,8 @@ const initialAirline = { iata: '', icao: '', numberCode: '', + sortDestinationID: 0, + sortDestinationName: '', }; export default initialAirline; @@ -17,10 +18,10 @@ export function useAirlineDataLayer() { const { showError } = useError(); const [airlines, setAirlines] = useState([]); const [addAirlineSuccess, setAddAirlineSuccess] = useState(false); - const [addAirlineServerSideResult, setAddAirlineServerSideResult] = useState(null); + const [addAirlineValidationProblemDetails, setAddAirlineValidationProblemDetails] = useState(null); const [deleteAirlineSuccess, setDeleteAirlineSuccess] = useState(false); const [updateAirlineSuccess, setUpdateAirlineSuccess] = useState(false); - const [updateAirlineServerSideResult, setUpdateAirlineServerSideResult] = useState(null); + const [updateAirlineValidationProblemDetails, setUpdateAirlineValidationProblemDetails] = useState(null); //The function adds an airline to the server. //@param {object} airline The airline to add. @@ -38,8 +39,11 @@ export function useAirlineDataLayer() { if (response.ok) { setAddAirlineSuccess(true); } - else if (response.status == 400) { - response.json().then(serverSideValidationResult => setAddAirlineServerSideResult(shapeValidationResult(serverSideValidationResult))); + else if (response.status === 400) { + response.json().then(validationProblemDetails => setAddAirlineValidationProblemDetails(validationProblemDetails)); + } + else if (response.status === 500) { + response.json().then(problemDetails => showError(problemDetails.detail)); } else { showError('Failed to create the airline because of an error on the server.'); @@ -50,11 +54,11 @@ export function useAirlineDataLayer() { //The function clears the states before an operation. const clearStates = () => { - setAddAirlineServerSideResult(null); setAddAirlineSuccess(false); + setAddAirlineValidationProblemDetails(null); setDeleteAirlineSuccess(false); - setUpdateAirlineServerSideResult(null); setUpdateAirlineSuccess(false); + setUpdateAirlineValidationProblemDetails(null); }; //The function deletes an airline from the server. @@ -69,6 +73,9 @@ export function useAirlineDataLayer() { if (response.ok) { setDeleteAirlineSuccess(true); } + else if (response.status === 500) { + response.json().then(problemDetails => showError(problemDetails.detail)); + } else { showError('Failed to delete the airline because of an error on the server.'); } @@ -100,11 +107,11 @@ export function useAirlineDataLayer() { if (response.ok) { setUpdateAirlineSuccess(true); } - else if (response.status == 400) { - response.json().then(serverSideValidationResult => setUpdateAirlineServerSideResult(shapeValidationResult(serverSideValidationResult))); + else if (response.status === 400) { + response.json().then(validationProblemDetails => setUpdateAirlineValidationProblemDetails(validationProblemDetails)); } - else if (response.status == 409) { - showError('The submitted data was detected to be out of date; please refresh and try again.'); + else if (response.status == 409 || response.status === 500) { + response.json().then(problemDetails => showError(problemDetails.detail)); } else { showError('Failed to update the airline because of an error on the server.'); @@ -117,12 +124,12 @@ export function useAirlineDataLayer() { airlines, getAirlines, addAirline, - addAirlineServerSideResult, + addAirlineValidationProblemDetails, addAirlineSuccess, deleteAirline, deleteAirlineSuccess, updateAirline, - updateAirlineServerSideResult, + updateAirlineValidationProblemDetails, updateAirlineSuccess }; }; \ No newline at end of file diff --git a/jmayer.example.aspreact.client/src/datalayers/DataLayerHelper.jsx b/jmayer.example.aspreact.client/src/datalayers/DataLayerHelper.jsx deleted file mode 100644 index dfd25d7..0000000 --- a/jmayer.example.aspreact.client/src/datalayers/DataLayerHelper.jsx +++ /dev/null @@ -1,25 +0,0 @@ -//The function shapes the validation result into a C# ServerSideValidationResult object. This is done -//because the server can send a ValidationProblemDetails object instead of a ServerSideValidationResult object; -//ASP.NET core checks data annotations and sends a ValidationProblemDetails object on failure. -export function shapeValidationResult(result) { - if (!Array.isArray(result.errors)) { - let newResult = { - errors: [] - }; - - //errors will be a dictionary where the property name is the key and the value is a list of error messages. - //The loop will convert the dictionary into an array of objects where each object is a property name and error message. - for (let key in result.errors) { - for (const errorMessage of result.errors[key]) { - newResult.errors.push({ - propertyName: key, - errorMessage: errorMessage - }); - } - } - - result = newResult; - } - - return result; -} \ No newline at end of file diff --git a/jmayer.example.aspreact.client/src/datalayers/FlightDataLayer.jsx b/jmayer.example.aspreact.client/src/datalayers/FlightDataLayer.jsx index 8675dd4..a5cdb1a 100644 --- a/jmayer.example.aspreact.client/src/datalayers/FlightDataLayer.jsx +++ b/jmayer.example.aspreact.client/src/datalayers/FlightDataLayer.jsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { useError } from '../components/errorDialog/ErrorProvider.jsx'; -import { shapeValidationResult } from './DataLayerHelper.jsx'; //Defines the initial flight object. const initialFlight = { @@ -22,11 +21,11 @@ export default initialFlight; export function useFlightDataLayer() { const { showError } = useError(); const [flights, setFlights] = useState([]); - const [addFlightServerSideResult, setAddFlightServerSideResult] = useState(null); const [addFlightSuccess, setAddFlightSuccess] = useState(false); + const [addFlightValidationProblemDetails, setAddFlightValidationProblemDetails] = useState(null); const [deleteFlightSuccess, setDeleteFlightSuccess] = useState(false); - const [updateFlightServerSideResult, setUpdateFlightServerSideResult] = useState(null); const [updateFlightSuccess, setUpdateFlightSuccess] = useState(false); + const [updateFlightValidationProblemDetails, setUpdateFlightValidationProblemDetails] = useState(null); //The function adds a flight to the server. //@param {object} flight The flight to add. @@ -46,8 +45,11 @@ export function useFlightDataLayer() { if (response.ok) { setAddFlightSuccess(true); } - else if (response.status == 400) { - response.json().then(serverSideValidationResult => setAddFlightServerSideResult(shapeValidationResult(serverSideValidationResult))); + else if (response.status === 400) { + response.json().then(validationProblemDetails => setAddFlightValidationProblemDetails(validationProblemDetails)); + } + else if (response.status === 500) { + response.json().then(problemDetails => showError(problemDetails.detail)); } else { showError('Failed to create the flight because of an error on the server.'); @@ -58,11 +60,11 @@ export function useFlightDataLayer() { //The function clears the states before an operation. const clearStates = () => { - setAddFlightServerSideResult(null); setAddFlightSuccess(false); + setAddFlightValidationProblemDetails(null); setDeleteFlightSuccess(false); - setUpdateFlightServerSideResult(null); setUpdateFlightSuccess(false); + setUpdateFlightValidationProblemDetails(null); }; //The function deletes a flight from the server. @@ -77,6 +79,9 @@ export function useFlightDataLayer() { if (response.ok) { setDeleteFlightSuccess(true); } + else if (response.status === 500) { + response.json().then(problemDetails => showError(problemDetails.detail)); + } else { showError('Failed to delete the flight because of an error on the server.'); } @@ -120,11 +125,11 @@ export function useFlightDataLayer() { if (response.ok) { setUpdateFlightSuccess(true); } - else if (response.status == 400) { - response.json().then(serverSideValidationResult => setUpdateFlightServerSideResult(shapeValidationResult(serverSideValidationResult))); + else if (response.status === 400) { + response.json().then(validationProblemDetails => setUpdateFlightValidationProblemDetails(validationProblemDetails)); } - else if (response.status == 409) { - showError('The submitted data was detected to be out of date; please refresh and try again.'); + else if (response.status == 409 || response.status === 500) { + response.json().then(problemDetails => showError(problemDetails.detail)); } else { showError('Failed to update the flight because of an error on the server.'); @@ -137,12 +142,12 @@ export function useFlightDataLayer() { flights, getFlights, addFlight, - addFlightServerSideResult, + addFlightValidationProblemDetails, addFlightSuccess, deleteFlight, deleteFlightSuccess, updateFlight, - updateFlightServerSideResult, + updateFlightValidationProblemDetails, updateFlightSuccess }; }; \ No newline at end of file