From 4713e291ac6d2ad1cfd1ced07d97993afa03e5d3 Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Fri, 20 Dec 2024 11:05:09 +0000 Subject: [PATCH] DRTII-1706 Connect to CWL & parse flights --- .../scala/drt/server/feeds/cwl/CWLFeed.scala | 346 +++++++++ .../test/scala/feeds/cwl/CWLFeedSpec.scala | 720 ++++++++++++++++++ 2 files changed, 1066 insertions(+) create mode 100644 server/src/main/scala/drt/server/feeds/cwl/CWLFeed.scala create mode 100644 server/src/test/scala/feeds/cwl/CWLFeedSpec.scala diff --git a/server/src/main/scala/drt/server/feeds/cwl/CWLFeed.scala b/server/src/main/scala/drt/server/feeds/cwl/CWLFeed.scala new file mode 100644 index 000000000..bf88280ad --- /dev/null +++ b/server/src/main/scala/drt/server/feeds/cwl/CWLFeed.scala @@ -0,0 +1,346 @@ +package drt.server.feeds.cwl + +import akka.actor.ActorSystem +import akka.http.scaladsl.{ConnectionContext, Http} +import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport +import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.unmarshalling.{FromResponseUnmarshaller, Unmarshal, Unmarshaller} +import akka.stream.Materializer +import akka.stream.scaladsl.{Sink, Source} +import akka.util.ByteString +import drt.server.feeds.Feed.FeedTick +import drt.server.feeds.{ArrivalsFeedFailure, ArrivalsFeedResponse, ArrivalsFeedSuccess} +import drt.shared.CrunchApi.MillisSinceEpoch +import org.slf4j.{Logger, LoggerFactory} +import sun.nio.cs.UTF_8 +import uk.gov.homeoffice.drt.arrivals.{FeedArrival, FlightCode, LiveArrival} +import uk.gov.homeoffice.drt.ports.Terminals.Terminal +import uk.gov.homeoffice.drt.time.SDate + +import java.security.SecureRandom +import javax.net.ssl.SSLContext +import scala.collection.immutable +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.{Await, Future} +import scala.concurrent.duration.DurationInt +import scala.util.Try +import scala.xml.{Node, NodeSeq} + +object CWLFeed { + val log: Logger = LoggerFactory.getLogger(getClass) + + def apply[FT](client: CWLClientLike, source: Source[FeedTick, FT]) + (implicit actorSystem: ActorSystem, materializer: Materializer): Source[ArrivalsFeedResponse, FT] = { + var initialRequest = true + source.mapAsync(1) { _ => + log.info(s"Requesting CWL Feed") + if (initialRequest) + client.initialFlights.map { + case s: ArrivalsFeedSuccess => + initialRequest = false + s + case f: ArrivalsFeedFailure => + f + } + else + client.updateFlights + } + } +} + +case class CWLFlight( + airline: String, + flightNumber: String, + departureAirport: String, + arrivalAirport: String, + aircraftTerminal: String, + status: String, + scheduledOnBlocks: String, + arrival: Boolean, + international: Boolean, + estimatedOnBlocks: Option[String] = None, + actualOnBlocks: Option[String] = None, + estimatedTouchDown: Option[String] = None, + actualTouchDown: Option[String] = None, + aircraftParkingPosition: Option[String] = None, + passengerGate: Option[String] = None, + seatCapacity: Option[Int] = None, + paxCount: Option[Int] = None, + codeShares: List[String] = Nil + ) + +final class SoapActionHeader(action: String) extends ModeledCustomHeader[SoapActionHeader] { + override def renderInRequests = true + + override def renderInResponses = false + + override val companion: ModeledCustomHeaderCompanion[SoapActionHeader] = SoapActionHeader + + override def value: String = action +} + +object SoapActionHeader extends ModeledCustomHeaderCompanion[SoapActionHeader] { + override val name = "SOAPAction" + + override def parse(value: String): Try[SoapActionHeader] = Try(new SoapActionHeader(value)) +} + +trait CWLClientLike extends ScalaXmlSupport { + val log: Logger = LoggerFactory.getLogger(getClass) + + val cwlLiveFeedUser: String + val soapEndPoint: String + + def initialFlights(implicit actorSystem: ActorSystem, materializer: Materializer): Future[ArrivalsFeedResponse] = { + + log.info(s"Making initial Live Feed Request") + sendXMLRequest(fullRefreshXml(cwlLiveFeedUser)) + } + + def updateFlights(implicit actorSystem: ActorSystem, materializer: Materializer): Future[ArrivalsFeedResponse] = { + + log.info(s"Making update Feed Request") + sendXMLRequest(updateXml()(cwlLiveFeedUser)) + } + + def sendXMLRequest(postXml: String)(implicit actorSystem: ActorSystem, materializer: Materializer): Future[ArrivalsFeedResponse] = { + + implicit val xmlToResUM: Unmarshaller[NodeSeq, CWLFlightsResponse] = CWLFlight.unmarshaller + implicit val resToCWLResUM: Unmarshaller[HttpResponse, CWLFlightsResponse] = CWLFlight.responseToAUnmarshaller + + val headers: List[HttpHeader] = List( + RawHeader("SOAPAction", "\"\""), + RawHeader("Accept", "*/*"), + RawHeader("Accept-Encoding", "gzip,deflate"), + ) + + println(s"Making request to $soapEndPoint\nHeaders: $headers\nPost XML: $postXml") + + makeRequest(soapEndPoint, headers, postXml) + .map { res => + log.info(s"Got a response from CWL ${res.status}") + val response = Unmarshal[HttpResponse](res).to[CWLFlightsResponse] + + response.map { + case s: CWLFlightsResponseSuccess => + ArrivalsFeedSuccess(s.flights.map(fs => CWLFlight.portFlightToArrival(fs))) + + case f: CWLFlightsResponseFailure => + ArrivalsFeedFailure(f.message) + } + } + .flatMap(identity) + .recoverWith { + case f => + log.error(s"Failed to get CWL Live Feed", f) + Future(ArrivalsFeedFailure(f.getMessage)) + } + } + + def fullRefreshXml: String => String = postXMLTemplate(fullRefresh = "1") + + def updateXml(): String => String = postXMLTemplate(fullRefresh = "0") + + def postXMLTemplate(fullRefresh: String)(username: String): String = + s""" + | + | + | $username + | $fullRefresh + | + |""".stripMargin + + def makeRequest(endpoint: String, headers: List[HttpHeader], postXML: String) + (implicit system: ActorSystem): Future[HttpResponse] + +} + +case class CWLClient(cwlLiveFeedUser: String, soapEndPoint: String) extends CWLClientLike { + + def makeRequest(endpoint: String, headers: List[HttpHeader], postXML: String) + (implicit system: ActorSystem): Future[HttpResponse] = { + val byteString: ByteString = ByteString.fromString(postXML, UTF_8.INSTANCE) + val contentType: ContentType = ContentTypes.`text/xml(UTF-8)` + val request = HttpRequest(HttpMethods.POST, endpoint, headers, HttpEntity(contentType, byteString)) + + val tls12Context = SSLContext.getInstance("TLSv1.2") + tls12Context.init(null, null, new SecureRandom()) + + Http().singleRequest(request, connectionContext = ConnectionContext.httpsClient(tls12Context)) + // .map { r => + // val x = r.entity.getDataBytes().map(_.utf8String).asScala.runWith(Sink.fold("")(_ + _)).map { body => + // println(s"Got response from CWL: ${r.status} $body") + // } + // Await.result(x, 30.seconds) + // r + // } + .recoverWith { + case f => + log.error(s"Failed to get CWL Live Feed: ${f.getMessage}") + Future.failed(f) + } + } +} + +trait NodeSeqUnmarshaller { + implicit def responseToAUnmarshaller[A](implicit resp: FromResponseUnmarshaller[NodeSeq], + toA: Unmarshaller[NodeSeq, A]): Unmarshaller[HttpResponse, A] = { + resp.flatMap(toA).asScala + } +} + +sealed trait CWLFlightsResponse + +case class CWLFlightsResponseSuccess(flights: List[CWLFlight]) extends CWLFlightsResponse + +case class CWLFlightsResponseFailure(message: String) extends CWLFlightsResponse + +object CWLFlight extends NodeSeqUnmarshaller { + def operationTimeFromNodeSeq(timeType: String, qualifier: String)(nodeSeq: NodeSeq): Option[String] = { + nodeSeq.find(p => + attributeFromNode(p, "OperationQualifier").contains(qualifier) && + attributeFromNode(p, "TimeType").contains(timeType) + ).map(_.text) + } + + def estTouchDown: NodeSeq => Option[String] = operationTimeFromNodeSeq("EST", "TDN") + + def actualTouchDown: NodeSeq => Option[String] = operationTimeFromNodeSeq("ACT", "TDN") + + def estChox: NodeSeq => Option[String] = operationTimeFromNodeSeq("EST", "ONB") + + def actualChox: NodeSeq => Option[String] = operationTimeFromNodeSeq("ACT", "ONB") + + val log: Logger = LoggerFactory.getLogger(getClass) + + def scheduledTime: NodeSeq => Option[String] = operationTimeFromNodeSeq("SCT", "ONB") + + implicit val unmarshaller: Unmarshaller[NodeSeq, CWLFlightsResponse] = Unmarshaller.strict[NodeSeq, CWLFlightsResponse] { xml => + + + val flightNodeSeq = xml \ "Body" \ "IATA_AIDX_FlightLegRS" \ "FlightLeg" + + println(s"Got ${flightNodeSeq.length} flights in CWL XML") + + val flights = flightNodeSeq + .filter { n => + (n \ "LegData" \ "AirportResources" \ "Resource").exists { p => + attributeFromNode(p, "DepartureOrArrival") == Option("Arrival") + } + } + .map { n => + val airline = (n \ "LegIdentifier" \ "Airline").text + val flightNumber = (n \ "LegIdentifier" \ "FlightNumber").text + val departureAirport = (n \ "LegIdentifier" \ "DepartureAirport").text + val aircraftTerminal = (n \ "LegData" \ "AirportResources" \ "Resource" \ "AircraftTerminal").text + val status = (n \ "LegData" \ "RemarkFreeText").text + val airportParkingLocation = maybeNodeText(n \ "LegData" \ "AirportResources" \ "Resource" \ "AircraftParkingPosition") + val passengerGate = maybeNodeText(n \ "LegData" \ "AirportResources" \ "Resource" \ "PassengerGate") + + val cabins = n \ "LegData" \ "CabinClass" + val maxPax = paxFromCabin(cabins, "SeatCapacity") + val totalPax = paxFromCabin(cabins, "PaxCount") + + val operationTimes = n \ "LegData" \ "OperationTime" + + val scheduledOnBlocks = scheduledTime(operationTimes).get + val maybeActualTouchDown = actualTouchDown(operationTimes) + val maybeEstTouchDown = estTouchDown(operationTimes) + val maybeEstChox = estChox(operationTimes) + val maybeActualChox = actualChox(operationTimes) + + CWLFlight( + airline, + flightNumber, + departureAirport, + "CWL", + aircraftTerminal, + status, + scheduledOnBlocks, + arrival = true, + international = true, + maybeEstChox, + maybeActualChox, + maybeEstTouchDown, + maybeActualTouchDown, + airportParkingLocation, + passengerGate, + maxPax, + totalPax + ) + }.toList + + val warningNode = xml \ "Body" \ "IATA_AIDX_FlightLegRS" \ "Warnings" \ "Warning" + + val warnings = warningNode.map(w => { + val typeCode = attributeFromNode(w, "Type").getOrElse("No error type code") + s"Code: $typeCode Message:${w.text}" + }) + warnings.foreach(w => log.warn(s"CWL Live Feed warning: $w")) + + if (flights.isEmpty && warnings.nonEmpty) + CWLFlightsResponseFailure(warnings.mkString(", ")) + else + CWLFlightsResponseSuccess(flights) + } + + def paxFromCabin(cabinPax: NodeSeq, seatingField: String): Option[Int] = cabinPax match { + case cpn if cpn.length > 0 => + val seats: immutable.Seq[Option[Int]] = cpn.flatMap(p => { + (p \ seatingField).map(seatingNode => + + if (seatingNode.text.isEmpty) + None + else + maybeNodeText(seatingNode).map(_.toInt) + ) + }) + if (seats.count(_.isDefined) > 0) + Option(seats.flatten.sum) + else + None + + case _ => None + } + + def maybeNodeText(n: NodeSeq): Option[String] = n.text match { + case t if t.nonEmpty => Option(t) + case _ => None + } + + def attributeFromNode(ot: Node, attributeName: String): Option[String] = ot.attribute(attributeName) match { + case Some(node) => Some(node.text) + case _ => None + } + + def portFlightToArrival(f: CWLFlight): FeedArrival = { + val (carrierCode, voyageNumber, suffix) = FlightCode.flightCodeToParts(f.airline + f.flightNumber) + + LiveArrival( + operator = Option(f.airline), + maxPax = f.seatCapacity, + totalPax = f.paxCount, + transPax = None, + terminal = Terminal(s"T${f.aircraftTerminal}"), + voyageNumber = voyageNumber.numeric, + carrierCode = carrierCode.code, + flightCodeSuffix = suffix.map(_.suffix), + origin = f.departureAirport, + scheduled = SDate(f.scheduledOnBlocks).millisSinceEpoch, + estimated = maybeTimeStringToMaybeMillis(f.estimatedTouchDown), + touchdown = maybeTimeStringToMaybeMillis(f.actualTouchDown), + estimatedChox = None, + actualChox = maybeTimeStringToMaybeMillis(f.actualOnBlocks), + status = f.status, + gate = f.passengerGate, + stand = f.aircraftParkingPosition, + runway = None, + baggageReclaim = None, + ) + } + + def maybeTimeStringToMaybeMillis(t: Option[String]): Option[MillisSinceEpoch] = t.flatMap( + SDate.tryParseString(_).toOption.map(_.millisSinceEpoch) + ) +} diff --git a/server/src/test/scala/feeds/cwl/CWLFeedSpec.scala b/server/src/test/scala/feeds/cwl/CWLFeedSpec.scala new file mode 100644 index 000000000..5c181c269 --- /dev/null +++ b/server/src/test/scala/feeds/cwl/CWLFeedSpec.scala @@ -0,0 +1,720 @@ +package feeds.cwl + +import akka.NotUsed +import akka.actor.ActorSystem +import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._ +import akka.http.scaladsl.model._ +import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller} +import akka.stream.Materializer +import akka.stream.scaladsl.{Sink, Source} +import akka.testkit.TestProbe +import drt.server.feeds.cwl.{CWLClient, CWLFlight, CWLFlightsResponse} +import drt.server.feeds.{ArrivalsFeedFailure, ArrivalsFeedResponse, ArrivalsFeedSuccess, Feed} +import services.crunch.CrunchTestLike +import uk.gov.homeoffice.drt.arrivals._ +import uk.gov.homeoffice.drt.ports.Terminals.T1 +import uk.gov.homeoffice.drt.ports.{LiveFeedSource, PortCode} +import uk.gov.homeoffice.drt.time.SDate + +import scala.collection.immutable +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} +import scala.xml.{NodeSeq, XML} + +class CWLFeedSpec extends CrunchTestLike { + sequential + isolated + + implicit val xmlToResUM: Unmarshaller[NodeSeq, CWLFlightsResponse] = CWLFlight.unmarshaller + implicit val resToCWLResUM: Unmarshaller[HttpResponse, CWLFlightsResponse] = CWLFlight.responseToAUnmarshaller + + "The CWL Feed client should successfully get a response from the CWL server" >> { + +// skipped("Exploratory test - requires VPN connection, correct feed url and username env vars") + + val endpoint = "https://webserviceqa.cwl.aero/AIDXReadFlight/SERVICES/RequestFlightService.svc" //sys.env("CWL_IATA_ENDPOINT_URL") + val username = sys.env("CWL_IATA_USERNAME") + + val cwlClient = CWLClient(username, endpoint) + Await.result(cwlClient.initialFlights, 30.seconds) + + false + } + +// "Given some flight xml with 1 flight, I should get get back a list of 1 arrival" >> { +// +// val resp = HttpResponse( +// entity = HttpEntity( +// contentType = ContentType(MediaTypes.`application/xml`, HttpCharsets.`UTF-8`), +// cwlSoapResponse1FlightXml +// ) +// ) +// +// val result = Await.result(Unmarshal[HttpResponse](resp).to[CWLFlightsResponse], 5.seconds) +// .asInstanceOf[CWLFlightsResponseSuccess] +// .flights +// val expected = List( +// CWLFlight( +// "TOM", +// "7623", +// "PFO", +// "CWL", +// "1", +// "ARR", +// "2018-09-01T23:00:00.000Z", +// arrival = true, +// international = true, +// None, +// Option("2018-09-01T23:05:00.000Z"), +// None, +// Option("2018-09-01T23:00:00.000Z"), +// Option("54L"), +// Option("44"), +// Option(189), +// Option(65) +// ) +// ) +// +// result === expected +// } +// +// val flight1: CWLFlight = CWLFlight( +// "TOM", +// "7623", +// "PFO", +// "CWL", +// "1", +// "ARR", +// "2018-09-01T23:00:00.000Z", +// arrival = true, +// international = true, +// None, +// Option("2018-09-01T23:05:00.000Z"), +// None, +// Option("2018-09-01T23:00:00.000Z"), +// Option("54L"), +// Option("44"), +// Option(189) +// ) +// +// val flight2: CWLFlight = CWLFlight( +// "FR", +// "8045", +// "CHQ", +// "CWL", +// "2", +// "ARR", +// "2018-09-18T23:00:00.000Z", +// arrival = true, +// international = true, +// None, +// Option("2018-09-18T23:05:00.000Z"), +// None, +// Option("2018-09-18T23:00:00.000Z"), +// Option("1"), +// Option("1"), +// Option(189) +// ) +// +// "Given some flight xml with 2 flights, I should get get back 2 arrival objects" >> { +// +// val resp = HttpResponse( +// entity = HttpEntity( +// contentType = ContentType(MediaTypes.`application/xml`, HttpCharsets.`UTF-8`), +// cwlSoapResponse2FlightsXml +// ) +// ) +// +// val result = Await.result(Unmarshal[HttpResponse](resp).to[CWLFlightsResponse], 5.seconds) +// .asInstanceOf[CWLFlightsResponseSuccess] +// .flights +// val expected = List( +// flight1, +// flight2 +// ) +// +// result === expected +// } +// +// "Given a flight with multiple types of passengers, those passenger numbers should be added together" >> { +// +// val resp = HttpResponse( +// entity = HttpEntity( +// contentType = ContentType(MediaTypes.`application/xml`, HttpCharsets.`UTF-8`), +// multiplePassengerTypesXML +// ) +// ) +// +// val result = Await.result(Unmarshal[HttpResponse](resp).to[CWLFlightsResponse], 5.seconds) +// .asInstanceOf[CWLFlightsResponseSuccess] +// .flights +// .head +// .paxCount +// +// val expected = Option(71) +// +// result === expected +// } +// +// case class CWLMockClient(xmlResponse: String, cwlLiveFeedUser: String = "", soapEndPoint: String = "") extends CWLClientLike { +// +// +// def makeRequest(endpoint: String, headers: List[HttpHeader], postXML: String) +// (implicit system: ActorSystem): Future[HttpResponse] = Future(HttpResponse( +// entity = HttpEntity( +// contentType = ContentType(MediaTypes.`application/xml`, HttpCharsets.`UTF-8`), +// xmlResponse +// ))) +// } +// +// "Given a request for a full refresh of all flights, if it's successful the client should return all the flights" >> { +// val client = CWLMockClient(cwlSoapResponse2FlightsXml) +// +// val result = Await +// .result(client.initialFlights, 1.second).asInstanceOf[ArrivalsFeedSuccess].arrivals +// val expected = List( +// CWLFlight.cwlFlightToArrival(flight1), +// CWLFlight.cwlFlightToArrival(flight2) +// ) +// +// result === expected +// } +// +// "Given a request for a full refresh of all flights, if we are rate limited then we should get an ArrivalsFeedFailure" >> { +// val client = CWLMockClient(rateLimitReachedResponse) +// +// val result = Await.result(client.initialFlights, 1.second) +// +// result must haveClass[ArrivalsFeedFailure] +// } +// +// "Given a mock client returning an invalid XML response I should get an ArrivalFeedFailure " >> { +// val client = CWLMockClient(invalidXmlResponse) +// +// val result = Await.result(client.initialFlights, 1.second) +// +// result must haveClass[ArrivalsFeedFailure] +// } +// +// case class CWLMockClientWithUpdates(initialResponses: List[ArrivalsFeedResponse], updateResponses: List[ArrivalsFeedResponse]) extends CWLClientLike { +// +// var mockInitialResponse: immutable.Seq[ArrivalsFeedResponse] = initialResponses +// var mockUpdateResponses: immutable.Seq[ArrivalsFeedResponse] = updateResponses +// +// override def initialFlights(implicit actorSystem: ActorSystem, materializer: Materializer): Future[ArrivalsFeedResponse] = mockInitialResponse match { +// case head :: tail => +// mockInitialResponse = tail +// Future(head) +// case Nil => +// Future(ArrivalsFeedFailure("No more mock esponses")) +// } +// +// override def updateFlights(implicit actorSystem: ActorSystem, materializer: Materializer): Future[ArrivalsFeedResponse] = +// mockUpdateResponses match { +// case head :: tail => +// mockUpdateResponses = tail +// Future(head) +// +// case Nil => +// Future(ArrivalsFeedFailure("No more mock esponses")) +// } +// +// def makeRequest(endpoint: String, headers: List[HttpHeader], postXML: String) +// (implicit system: ActorSystem): Future[HttpResponse] = ??? +// +// override val cwlLiveFeedUser: String = "" +// override val soapEndPoint: String = "" +// } +// +// "Given a request for a full refresh of all flights fails, we should poll for a full request until it succeeds" >> { +// val firstFailure = ArrivalsFeedFailure("First Failure") +// val secondFailure = ArrivalsFeedFailure("Second Failure") +// val finallySuccess = ArrivalsFeedSuccess(List()) +// +// val initialResponses = List(firstFailure, secondFailure, finallySuccess) +// val updateResponses = List(finallySuccess) +// +// val feed = CWLFeed( +// CWLMockClientWithUpdates(initialResponses, updateResponses), +// Feed.actorRefSource +// ) +// +// val probe = TestProbe() +// val expected = Seq(firstFailure, secondFailure, finallySuccess, finallySuccess) +// val actorSource = feed.take(4).to(Sink.actorRef(probe.ref, NotUsed)).run() +// Source(1 to 4).map(_ => actorSource ! Feed.Tick).run() +// +// probe.receiveN(4, 1.second) === expected +// } +// +// "Given a successful initial request, followed by a failed update, we should continue to poll for updates" >> { +// +// val failure = ArrivalsFeedFailure("First Failure") +// val finallySuccess = ArrivalsFeedSuccess(List()) +// +// val initialResponses = List(finallySuccess) +// val updateResponses = List(failure, finallySuccess) +// +// val feed = CWLFeed( +// CWLMockClientWithUpdates(initialResponses, updateResponses), +// Feed.actorRefSource +// ) +// +// val expected = Seq(finallySuccess, failure, finallySuccess) +// val probe = TestProbe() +// val actorSource = feed.take(3).to(Sink.actorRef(probe.ref, NotUsed)).run() +// Source(1 to 3).map(_ => actorSource ! Feed.Tick).run() +// +// probe.receiveN(3, 1.second) === expected +// } +// +// "Given a list of operation times I should be able to extract the scheduled time" >> { +// val xml = +// XML.loadString( +// """ +// | +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T23:00:00.000Z +// | +// """.stripMargin) +// +// val expected = "2018-09-01T23:00:00.000Z" +// val node = xml \ "OperationTime" +// val result = CWLFlight.scheduledTime(node).get +// +// result === expected +// } +// +// "Given a list of operation times I should be able to extract the actual chox time" >> { +// val xml = +// XML.loadString( +// """ +// | +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T24:00:00.000Z +// | +// """.stripMargin) +// +// +// val expected = "2018-09-01T24:00:00.000Z" +// val node = xml \ "OperationTime" +// val result = CWLFlight.actualChox(node).get +// +// result === expected +// } +// +// "Given a list of operation times I should be able to extract the estimated chox time" >> { +// val xml = +// XML.loadString( +// """ +// | +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T24:00:00.000Z +// | +// """.stripMargin) +// +// val expected = "2018-09-01T24:00:00.000Z" +// val node = xml \ "OperationTime" +// val result = CWLFlight.estChox(node).get +// +// result === expected +// } +// +// "Given a list of operation times I should be able to extract the estimated touchdown time" >> { +// val xml = +// XML.loadString( +// """ +// | +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T24:00:00.000Z +// | +// """.stripMargin) +// +// val expected = "2018-09-01T24:00:00.000Z" +// val node = xml \ "OperationTime" +// val result = CWLFlight.estTouchDown(node).get +// +// result === expected +// } +// +// "Given a list of operation times I should be able to extract the actual touchdown time" >> { +// val xml = +// XML.loadString( +// """ +// | +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T24:00:00.000Z +// | +// """.stripMargin) +// +// +// val expected = "2018-09-01T24:00:00.000Z" +// val node = xml \ "OperationTime" +// val result = CWLFlight.actualTouchDown(node).get +// +// result === expected +// } +// +// "Given a CWLFlight, I should get an Arrival back with the same fields - we should not use Est Chox" >> { +// val estimatedOnBlocksTimeString = "2018-09-01T23:05:00.000Z" +// val actualOnBlocksTimeString = "2018-09-01T23:06:00.000Z" +// val estimatedTouchDownTimeString = "2018-09-01T23:07:00.000Z" +// val actualTouchDownTimeString = "2018-09-01T23:08:00.000Z" +// val scheduledTimeString = "2018-09-01T23:00:00.000Z" +// +// val cwlFlight = CWLFlight( +// "SA", +// "123", +// "JNB", +// "CWL", +// "1", +// "ARR", +// scheduledTimeString, +// arrival = true, +// international = true, +// Option(estimatedOnBlocksTimeString), +// Option(actualOnBlocksTimeString), +// Option(estimatedTouchDownTimeString), +// Option(actualTouchDownTimeString), +// Option("55"), +// Option("6"), +// Option(175), +// Option(65), +// Nil +// ) +// +// val result = CWLFlight.cwlFlightToArrival(cwlFlight) +// +// val expected = LiveArrival( +// operator = Option("SA"), +// maxPax = Option(175), +// totalPax = Option(65), +// transPax = None, +// terminal = T1, +// voyageNumber = 123, +// carrierCode = "SA", +// flightCodeSuffix = None, +// origin = "JNB", +// scheduled = SDate(scheduledTimeString).millisSinceEpoch, +// estimated = Option(SDate(estimatedTouchDownTimeString).millisSinceEpoch), +// touchdown = Option(SDate(actualTouchDownTimeString).millisSinceEpoch), +// estimatedChox = None, +// actualChox = Option(SDate(actualOnBlocksTimeString).millisSinceEpoch), +// status = "ARR", +// gate = Option("6"), +// stand = Option("55"), +// runway = None, +// baggageReclaim = None, +// ) +// +// result === expected +// } +// +// "Given a CWLFlight with 0 for passenger fields, I should see 0 pax, 0 max pax and 0 transfer pax." >> { +// val client = CWLMockClient(cwlSoapResponseWith0PaxXml) +// +// val result = Await +// .result(client.initialFlights, 1.second).asInstanceOf[ArrivalsFeedSuccess].arrivals +// +// val actMax = result match { +// case f :: _ => (f.totalPax, f.maxPax) +// } +// +// val expected = (Some(0), Some(0)) +// +// actMax === expected +// } +// +// def multiplePassengerTypesXML: String = +// """ +// | +// | +// | +// | +// | +// | SN +// | 1234 +// | TST +// | CWL +// | 2019-08-05 +// | +// | +// | +// | +// | +// | +// | +// | 68 +// | 1 +// | 1 +// | 1 +// | 88 +// | +// | ARR +// | +// | +// | +// | 5 +// | 0 +// | +// | 1 +// | +// | +// | 2018-09-01T23:00:00.000Z +// | +// | +// | +// | +// | +// | +// | +// | +// | +// """.stripMargin +// +// def cwlSoapResponse1FlightXml: String = +// """ +// | +// | +// | +// | +// | +// | TOM +// | 7623 +// | PFO +// | CWL +// | 2018-09-01 +// | +// | +// | +// | +// | C +// | +// | +// | +// | 189 +// | 65 +// | +// | ARR +// | +// | +// | +// | 54L +// | 44 +// | +// | 1 +// | 3 +// | +// | +// | +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T23:05:00.000Z +// | +// | 73H +// | +// | +// | +// | S +// | +// | +// | +// | +// | +// | Thomson Airways +// | Paphos +// | Birmingham +// | +// | +// | +// | +// | +// """.stripMargin +// +// def cwlSoapResponseWith0PaxXml: String = +// """ +// | +// | +// | +// | +// | +// | TOM +// | 7623 +// | PFO +// | CWL +// | 2018-09-01 +// | +// | +// | +// | +// | C +// | +// | +// | +// | 0 +// | 0 +// | +// | ARR +// | +// | +// | +// | 54L +// | 44 +// | +// | 1 +// | 3 +// | +// | +// | +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T23:05:00.000Z +// | +// | 73H +// | +// | +// | +// | S +// | +// | +// | +// | +// | +// | Thomson Airways +// | Paphos +// | Birmingham +// | +// | +// | +// | +// | +// """.stripMargin +// +// def cwlSoapResponse2FlightsXml: String = +// """ +// | +// | +// | +// | +// | +// | TOM +// | 7623 +// | PFO +// | CWL +// | 2018-09-01 +// | +// | +// | +// | +// | C +// | +// | +// | +// | 189 +// | +// | ARR +// | +// | +// | +// | 54L +// | 44 +// | +// | 1 +// | 3 +// | +// | +// | +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T23:00:00.000Z +// | 2018-09-01T23:05:00.000Z +// | +// | 73H +// | +// | +// | +// | S +// | +// | +// | +// | +// | +// | Thomson Airways +// | Paphos +// | Birmingham +// | +// | +// | +// | +// | FR +// | 8045 +// | CHQ +// | CWL +// | 2018-09-18 +// | +// | +// | +// | +// | J +// | +// | +// | +// | 180 +// | +// | +// | 9 +// | +// | ARR +// | +// | +// | +// | 1 +// | 1 +// | +// | 2 +// | 7 +// | +// | +// | +// | 2018-09-18T23:00:00.000Z +// | 2018-09-18T23:00:00.000Z +// | 2018-09-18T23:05:00.000Z +// | +// | 73H +// | +// | +// | +// | S +// | +// | +// | +// | +// | +// | Ryanair +// | Chania (s) +// | Birmingham +// | +// | +// | +// | +// | +// """.stripMargin +// +// def rateLimitReachedResponse: String = +// """ +// | +// | +// | +// | +// | +// | Warning: Full Refresh not possible at this time please try in 590 seconds. +// | +// | +// | +// | +// """.stripMargin +// +// def invalidXmlResponse: String = +// """ +// |Blah blah +// """.stripMargin +}