diff --git a/project/Settings.scala b/project/Settings.scala index 27dda2c33..2f6edfdad 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -25,7 +25,7 @@ object Settings { /** Declare global dependency versions here to avoid mismatches in multi part dependencies */ //noinspection ScalaStyle object versions { - val drtLib = "v993" + val drtLib = "v1009" val scala = "2.13.12" val scalaDom = "2.8.0" diff --git a/server/src/main/resources/config/feeds.conf b/server/src/main/resources/config/feeds.conf index a5ac77702..41a643fb4 100644 --- a/server/src/main/resources/config/feeds.conf +++ b/server/src/main/resources/config/feeds.conf @@ -72,6 +72,15 @@ feeds { } } + cwl { + iata { + endPointUrl = "" + endPointUrl = ${?CWL_IATA_ENDPOINT_URL} + username = "" + username = ${?CWL_IATA_USERNAME} + } + } + ltn { live { url = ${?LTN_LIVE_URL} diff --git a/server/src/main/scala/actors/ProdDrtParameters.scala b/server/src/main/scala/actors/ProdDrtParameters.scala index ac3e42451..7fae40859 100644 --- a/server/src/main/scala/actors/ProdDrtParameters.scala +++ b/server/src/main/scala/actors/ProdDrtParameters.scala @@ -27,6 +27,9 @@ trait DrtParameters { val bhxIataUsername: String val maybeBhxSoapEndPointUrl: Option[String] + val cwlIataEndPointUrl: String + val cwlIataUsername: String + val maybeLtnLiveFeedUrl: Option[String] val maybeLtnLiveFeedUsername: Option[String] val maybeLtnLiveFeedPassword: Option[String] @@ -97,6 +100,9 @@ case class ProdDrtParameters @Inject()(config: Configuration) extends DrtParamet override val bhxIataEndPointUrl: String = config.get[String]("feeds.bhx.iata.endPointUrl") override val bhxIataUsername: String = config.get[String]("feeds.bhx.iata.username") + override val cwlIataEndPointUrl: String = config.get[String]("feeds.cwl.iata.endPointUrl") + override val cwlIataUsername: String = config.get[String]("feeds.cwl.iata.username") + override val maybeBhxSoapEndPointUrl: Option[String] = config.getOptional[String]("feeds.bhx.soap.endPointUrl") override val maybeLtnLiveFeedUrl: Option[String] = config.getOptional[String]("feeds.ltn.live.url") diff --git a/server/src/main/scala/drt/server/feeds/cwl/CWLFeed.scala b/server/src/main/scala/drt/server/feeds/cwl/CWLFeed.scala index bf88280ad..59828a71c 100644 --- a/server/src/main/scala/drt/server/feeds/cwl/CWLFeed.scala +++ b/server/src/main/scala/drt/server/feeds/cwl/CWLFeed.scala @@ -1,13 +1,13 @@ 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.http.scaladsl.{ConnectionContext, Http} import akka.stream.Materializer -import akka.stream.scaladsl.{Sink, Source} +import akka.stream.scaladsl.Source import akka.util.ByteString import drt.server.feeds.Feed.FeedTick import drt.server.feeds.{ArrivalsFeedFailure, ArrivalsFeedResponse, ArrivalsFeedSuccess} @@ -22,8 +22,7 @@ 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.concurrent.Future import scala.util.Try import scala.xml.{Node, NodeSeq} @@ -115,8 +114,6 @@ trait CWLClientLike extends ScalaXmlSupport { 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}") @@ -124,7 +121,8 @@ trait CWLClientLike extends ScalaXmlSupport { response.map { case s: CWLFlightsResponseSuccess => - ArrivalsFeedSuccess(s.flights.map(fs => CWLFlight.portFlightToArrival(fs))) + val arrivals = s.flights.map(fs => CWLFlight.portFlightToArrival(fs)) + ArrivalsFeedSuccess(arrivals) case f: CWLFlightsResponseFailure => ArrivalsFeedFailure(f.message) @@ -167,14 +165,8 @@ case class CWLClient(cwlLiveFeedUser: String, soapEndPoint: String) extends CWLC 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 - // } + Http() + .singleRequest(request, connectionContext = ConnectionContext.httpsClient(tls12Context)) .recoverWith { case f => log.error(s"Failed to get CWL Live Feed: ${f.getMessage}") @@ -221,8 +213,6 @@ object CWLFlight extends NodeSeqUnmarshaller { 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 => @@ -322,7 +312,7 @@ object CWLFlight extends NodeSeqUnmarshaller { maxPax = f.seatCapacity, totalPax = f.paxCount, transPax = None, - terminal = Terminal(s"T${f.aircraftTerminal}"), + terminal = Terminal(f.aircraftTerminal), voyageNumber = voyageNumber.numeric, carrierCode = carrierCode.code, flightCodeSuffix = suffix.map(_.suffix), diff --git a/server/src/main/scala/uk/gov/homeoffice/drt/service/ProdFeedService.scala b/server/src/main/scala/uk/gov/homeoffice/drt/service/ProdFeedService.scala index f719f35d0..3c67c52eb 100644 --- a/server/src/main/scala/uk/gov/homeoffice/drt/service/ProdFeedService.scala +++ b/server/src/main/scala/uk/gov/homeoffice/drt/service/ProdFeedService.scala @@ -24,6 +24,7 @@ import drt.server.feeds.bhx.{BHXClient, BHXFeed} import drt.server.feeds.chroma.ChromaLiveFeed import drt.server.feeds.cirium.CiriumFeed import drt.server.feeds.common.{ManualUploadArrivalFeed, ProdHttpClient} +import drt.server.feeds.cwl.{CWLClient, CWLFeed} import drt.server.feeds.edi.EdiFeed import drt.server.feeds.gla.GlaFeed import drt.server.feeds.lcy.{LCYClient, LCYFeed} @@ -429,6 +430,8 @@ case class ProdFeedService(journalType: StreamingJournalLike, Feed(LGWFeed(azureClient)(system).source(Feed.actorRefSource), 5.seconds, 100.milliseconds) case "BHX" if params.bhxIataEndPointUrl.nonEmpty => Feed(BHXFeed(BHXClient(params.bhxIataUsername, params.bhxIataEndPointUrl), Feed.actorRefSource), 5.seconds, 80.seconds) + case "CWL" if params.cwlIataEndPointUrl.nonEmpty => + Feed(CWLFeed(CWLClient(params.cwlIataUsername, params.cwlIataEndPointUrl), Feed.actorRefSource), 5.seconds, 80.seconds) case "LCY" if params.lcyLiveEndPointUrl.nonEmpty => Feed(LCYFeed(LCYClient(ProdHttpClient(), params.lcyLiveUsername, params.lcyLiveEndPointUrl, params.lcyLiveUsername, params.lcyLivePassword), Feed.actorRefSource), 5.seconds, 80.seconds) case "LTN" => diff --git a/server/src/main/scala/uk/gov/homeoffice/drt/testsystem/TestTables.scala b/server/src/main/scala/uk/gov/homeoffice/drt/testsystem/TestTables.scala index 93b480504..795fe532b 100644 --- a/server/src/main/scala/uk/gov/homeoffice/drt/testsystem/TestTables.scala +++ b/server/src/main/scala/uk/gov/homeoffice/drt/testsystem/TestTables.scala @@ -100,6 +100,8 @@ case class MockDrtParameters @Inject()() extends DrtParameters { override val isSuperUserMode: Boolean = false override val bhxIataEndPointUrl: String = "" override val bhxIataUsername: String = "" + override val cwlIataEndPointUrl: String = "" + override val cwlIataUsername: String = "" override val maybeBhxSoapEndPointUrl: Option[String] = None override val maybeLtnLiveFeedUrl: Option[String] = None override val maybeLtnLiveFeedUsername: Option[String] = None diff --git a/server/src/test/scala/feeds/cwl/CWLFeedSpec.scala b/server/src/test/scala/feeds/cwl/CWLFeedSpec.scala index 5c181c269..ac1a61308 100644 --- a/server/src/test/scala/feeds/cwl/CWLFeedSpec.scala +++ b/server/src/test/scala/feeds/cwl/CWLFeedSpec.scala @@ -8,13 +8,11 @@ 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.cwl._ 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 uk.gov.homeoffice.drt.ports.Terminals.{T1, T2} import scala.collection.immutable import scala.concurrent.duration._ @@ -29,10 +27,9 @@ class CWLFeedSpec extends CrunchTestLike { 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") -// 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 endpoint = sys.env("CWL_IATA_ENDPOINT_URL") val username = sys.env("CWL_IATA_USERNAME") val cwlClient = CWLClient(username, endpoint) @@ -41,680 +38,538 @@ class CWLFeedSpec extends CrunchTestLike { 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 + "Given some flight xml with 1 flight with multiple passenger types, I should get 1 arrival with passengers summed" >> { + + 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 + val expected = List( + CWLFlight( + airline = "AA", + flightNumber = "1234", + departureAirport = "CDG", + arrivalAirport = "CWL", + aircraftTerminal = "T1", + status = "LBG", + scheduledOnBlocks = "2024-12-23T09:45:00.000Z", + arrival = true, + international = true, + estimatedOnBlocks = Some("2024-12-23T09:55:00.000Z"), + actualOnBlocks = Option("2024-12-23T09:59:00.000Z"), + estimatedTouchDown = None, + actualTouchDown = Option("2024-12-23T09:52:54.000Z"), + aircraftParkingPosition = Option("10"), + passengerGate = Option("10"), + seatCapacity = Option(88), + paxCount = Option(55) + ) + ) + + result === expected + } + + "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.size + + result === 2 + } + + "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 + + result === Option(55) + } + + "Given only a departure flight no flights should be returned" >> { + val resp = HttpResponse( + entity = HttpEntity( + contentType = ContentType(MediaTypes.`application/xml`, HttpCharsets.`UTF-8`), + departureFlightXML + ) + ) + + val result = Await.result(Unmarshal[HttpResponse](resp).to[CWLFlightsResponse], 5.seconds) + .asInstanceOf[CWLFlightsResponseSuccess] + .flights.size + + result === 0 + } + + 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 + + result === List( + LiveArrival(Some("TOM"), Some(189), None, None, T1, 7623, "TOM", None, "PFO", 1535842800000L, None, Some(1535842800000L), None, Some(1535843100000L), "ARR", Some("44"), Some("54L"), None, None), + LiveArrival(Some("FR"), Some(189), None, None, T2, 8045, "FR", None, "CHQ", 1537311600000L, None, Some(1537311600000L), None, Some(1537311900000L), "ARR", Some("1"), Some("1"), None, None) + ) + } + + + "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 + } + + def multiplePassengerTypesXML: String = + """ + | + | + | + | + | + | AA + | 1234 + | CDG + | CWL + | 2024-12-23 + | + | + | + | + | J + | + | + | + | 50 + | 5 + | 88 + | + | + | DL + | 1111 + | + | + | EY + | 2222 + | + | + | KQ + | 3333 + | + | + | VS + | 4444 + | + | LBG + | + | + | + | 10 + | 10 + | 30 + | T1 + | A + | + | + | 2024-12-23T09:45:00.000Z + | 2024-12-23T09:55:00.000Z + | 2024-12-23T09:52:54.000Z + | 2024-12-23T09:59:00.000Z + | + | E90 + | + | PHEZY + | + | SER + | + | KLM69Y + | + | + | + | + | + | + | + """.stripMargin + + def departureFlightXML: String = + """ + | + | + | + | + | + | RUK + | 9431 + | CWL + | BFS + | 2023-07-02 + | + | + | + | + | J + | + | + | + | + | + | DEP + | + | + | + | 11 + | 30 + | + | + | + | 2023-07-02T13:02:00.000Z + | 2023-07-02T13:02:00.000Z + | 2023-07-02T12:57:00.000Z + | + | 73H + | + | GRUKK + | + | NON + | + | RUK9431 + | + | + | + | + | + | + | + """.stripMargin + + def cwlSoapResponse2FlightsXml: String = + """ + | + | + | + | + | + | TOM + | 7623 + | PFO + | CWL + | 2018-09-01 + | + | + | + | + | C + | + | + | + | 189 + | + | ARR + | + | + | + | 54L + | 44 + | + | T1 + | 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 + | + | T2 + | 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 }