Skip to content

Commit

Permalink
APID-72: make destination URL come from the WSDL instead of from conf…
Browse files Browse the repository at this point in the history
…ig (#28)
  • Loading branch information
worthydolt authored Mar 23, 2021
1 parent 630b05e commit c77a210
Show file tree
Hide file tree
Showing 13 changed files with 92 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ class AppConfig @Inject()(config: Configuration, servicesConfig: ServicesConfig)
val auditingEnabled: Boolean = config.get[Boolean]("auditing.enabled")
val graphiteHost: String = config.get[String]("microservice.metrics.graphite.host")

val ccn2Url: String = config.get[String]("ccn2Url")
val ccn2Username: String = config.get[String]("ccn2Username")
val ccn2Password: String = config.get[String]("ccn2Password")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,25 @@ package uk.gov.hmrc.apiplatformoutboundsoap.connectors

import play.api.http.HeaderNames.CONTENT_TYPE
import play.api.{Logger, LoggerLike}
import uk.gov.hmrc.apiplatformoutboundsoap.config.AppConfig
import uk.gov.hmrc.apiplatformoutboundsoap.models.SoapRequest
import uk.gov.hmrc.http.HttpReads.Implicits._
import uk.gov.hmrc.http.{HttpClient, _}

import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}

@Singleton
class OutboundConnector @Inject()(appConfig: AppConfig, httpClient: HttpClient)
class OutboundConnector @Inject()(httpClient: HttpClient)
(implicit ec: ExecutionContext) extends HttpErrorFunctions {

val logger: LoggerLike = Logger

def postMessage(message: String): Future[Int] = {
def postMessage(soapRequest: SoapRequest): Future[Int] = {
implicit val hc: HeaderCarrier = HeaderCarrier().withExtraHeaders(CONTENT_TYPE -> "application/soap+xml")

httpClient.POSTString[HttpResponse](appConfig.ccn2Url, message) map { response =>
httpClient.POSTString[HttpResponse](soapRequest.destinationUrl, soapRequest.soapEnvelope) map { response =>
if (!is2xx(response.status)) {
logger.warn(s"Attempted request to ${appConfig.ccn2Url} responded with HTTP response code ${response.status}")
logger.warn(s"Attempted request to ${soapRequest.destinationUrl} responded with HTTP response code ${response.status}")
}
response.status
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ sealed trait OutboundSoapMessage {
val globalId: UUID
val messageId: Option[String]
val soapMessage: String
val destinationUrl: String
val status: SendingStatus
val createDateTime: DateTime
val notificationUrl: Option[String]
Expand All @@ -52,6 +53,7 @@ object OutboundSoapMessage {
case class SentOutboundSoapMessage(globalId: UUID,
messageId: Option[String],
soapMessage: String,
destinationUrl: String,
createDateTime: DateTime,
ccnHttpStatus: Int,
notificationUrl: Option[String] = None) extends OutboundSoapMessage {
Expand All @@ -61,6 +63,7 @@ case class SentOutboundSoapMessage(globalId: UUID,
case class FailedOutboundSoapMessage(globalId: UUID,
messageId: Option[String],
soapMessage: String,
destinationUrl: String,
createDateTime: DateTime,
ccnHttpStatus: Int,
notificationUrl: Option[String] = None) extends OutboundSoapMessage {
Expand All @@ -70,14 +73,15 @@ case class FailedOutboundSoapMessage(globalId: UUID,
case class RetryingOutboundSoapMessage(globalId: UUID,
messageId: Option[String],
soapMessage: String,
destinationUrl: String,
createDateTime: DateTime,
retryDateTime: DateTime,
ccnHttpStatus: Int,
notificationUrl: Option[String] = None) extends OutboundSoapMessage {
override val status: SendingStatus = SendingStatus.RETRYING

def toFailed = FailedOutboundSoapMessage(globalId, messageId, soapMessage, createDateTime, ccnHttpStatus, notificationUrl)
def toSent = SentOutboundSoapMessage(globalId, messageId, soapMessage, createDateTime, ccnHttpStatus, notificationUrl)
def toFailed = FailedOutboundSoapMessage(globalId, messageId, soapMessage, destinationUrl, createDateTime, ccnHttpStatus, notificationUrl)
def toSent = SentOutboundSoapMessage(globalId, messageId, soapMessage, destinationUrl, createDateTime, ccnHttpStatus, notificationUrl)
}

sealed trait SendingStatus extends EnumEntry
Expand Down
19 changes: 19 additions & 0 deletions app/uk/gov/hmrc/apiplatformoutboundsoap/models/SoapRequest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2021 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.hmrc.apiplatformoutboundsoap.models

case class SoapRequest(soapEnvelope: String, destinationUrl: String)
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import akka.stream.scaladsl.Source
import org.joda.time.DateTime
import org.joda.time.DateTime.now
import org.joda.time.DateTimeZone.UTC
import play.api.libs.json.Json
import play.api.libs.json.{JsObject, Json}
import play.modules.reactivemongo.ReactiveMongoComponent
import reactivemongo.akkastream.cursorProducer
import reactivemongo.api.ReadPreference
Expand Down Expand Up @@ -64,7 +64,7 @@ class OutboundMessageRepository @Inject()(mongoComponent: ReactiveMongoComponent

collection
.find(Json.obj("status" -> SendingStatus.RETRYING.entryName,
"retryDateTime" -> Json.obj("$lte" -> now(UTC))), Option.empty[OutboundSoapMessage])
"retryDateTime" -> Json.obj("$lte" -> now(UTC))), Option.empty[JsObject])
.sort(Json.obj("retryDateTime" -> 1))
.cursor[RetryingOutboundSoapMessage](ReadPreference.primaryPreferred)
.documentSource()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ import uk.gov.hmrc.http.{HeaderCarrier, HttpErrorFunctions, NotFoundException}

import java.util.UUID
import javax.inject.{Inject, Singleton}
import javax.wsdl.extensions.soap12.SOAP12Address
import javax.wsdl.xml.WSDLReader
import javax.wsdl.{Definition, Operation, Part, PortType}
import javax.wsdl.{Definition, Operation, Part, PortType, Service, Port}
import javax.xml.namespace.QName
import scala.collection.JavaConverters._
import scala.concurrent.{ExecutionContext, Future}
Expand All @@ -61,9 +62,9 @@ class OutboundService @Inject()(outboundConnector: OutboundConnector,

def sendMessage(message: MessageRequest): Future[OutboundSoapMessage] = {
for {
envelope <- buildEnvelope(message)
result <- outboundConnector.postMessage(envelope)
outboundSoapMessage = buildOutboundSoapMessage(message, envelope, result)
soapRequest <- buildSoapRequest(message)
result <- outboundConnector.postMessage(soapRequest)
outboundSoapMessage = buildOutboundSoapMessage(message, soapRequest, result)
_ <- outboundMessageRepository.persist(outboundSoapMessage)
} yield outboundSoapMessage
}
Expand All @@ -74,7 +75,7 @@ class OutboundService @Inject()(outboundConnector: OutboundConnector,

private def retryMessage(message: RetryingOutboundSoapMessage)(implicit hc: HeaderCarrier): Future[Unit] = {
val nextRetryDateTime: DateTime = now.plus(appConfig.retryInterval.toMillis)
outboundConnector.postMessage(message.soapMessage) flatMap { result =>
outboundConnector.postMessage(SoapRequest(message.soapMessage, message.destinationUrl)) flatMap { result =>
if (is2xx(result)) {
logger.info(s"Retrying message with global ID ${message.globalId} and message ID ${message.messageId} succeeded")
outboundMessageRepository.updateStatus(message.globalId, SendingStatus.SENT) map { updatedMessage =>
Expand All @@ -96,19 +97,20 @@ class OutboundService @Inject()(outboundConnector: OutboundConnector,
}
}

private def buildOutboundSoapMessage(message: MessageRequest, envelope: String, result: Int): OutboundSoapMessage = {
private def buildOutboundSoapMessage(message: MessageRequest, soapRequest: SoapRequest, result: Int): OutboundSoapMessage = {
val globalId: UUID = randomUUID
val messageId = message.addressing.flatMap(_.messageId)
if (is2xx(result)) {
logger.info(s"Message with global ID $globalId and message ID $messageId successfully sent")
SentOutboundSoapMessage(globalId, messageId, envelope, now, result, message.notificationUrl)
SentOutboundSoapMessage(globalId, messageId, soapRequest.soapEnvelope, soapRequest.destinationUrl , now, result, message.notificationUrl)
} else {
logger.info(s"Message with global ID $globalId and message ID $messageId failed on first attempt")
RetryingOutboundSoapMessage(globalId, messageId, envelope, now, now.plus(appConfig.retryInterval.toMillis), result, message.notificationUrl)
RetryingOutboundSoapMessage(globalId, messageId, soapRequest.soapEnvelope, soapRequest.destinationUrl, now,
now.plus(appConfig.retryInterval.toMillis), result, message.notificationUrl)
}
}

private def buildEnvelope(message: MessageRequest): Future[String] = {
private def buildSoapRequest(message: MessageRequest): Future[SoapRequest] = {
cache.getOrElseUpdate[Definition](message.wsdlUrl, appConfig.cacheDuration) {
parseWsdl(message.wsdlUrl)
} map { wsdlDefinition: Definition =>
Expand All @@ -119,7 +121,12 @@ class OutboundService @Inject()(outboundConnector: OutboundConnector,
val envelope: SOAPEnvelope = getSOAP12Factory.getDefaultEnvelope
addHeaders(message, operation, envelope)
addBody(message, operation, envelope)
wsSecurityService.addUsernameToken(envelope)
val enrichedEnvelope: String = wsSecurityService.addUsernameToken(envelope)
val url: String = wsdlDefinition.getAllServices.asScala.values.head.asInstanceOf[Service]
.getPorts.asScala.values.head.asInstanceOf[Port]
.getExtensibilityElements.asScala.filter(_.isInstanceOf[SOAP12Address]).head.asInstanceOf[SOAP12Address]
.getLocationURI
SoapRequest(enrichedEnvelope, url)
}
}

Expand Down
1 change: 0 additions & 1 deletion conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ mongodb {
uri = "mongodb://localhost:27017/api-platform-outbound-soap"
}

ccn2Url = "http://localhost:6704/destination/notifications"
ccn2Username = "joe.bloggs"
ccn2Password = "foobar"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class NotificationCallbackConnectorISpec extends AnyWordSpec with Matchers with
"sendNotification" should {
"successfully send a status update to the caller's notification URL" in new Setup {
val message: OutboundSoapMessage = RetryingOutboundSoapMessage(
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", now, now, httpStatus, Some(wireMockBaseUrlAsString))
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", "some url", now, now, httpStatus, Some(wireMockBaseUrlAsString))
val expectedStatus: Int = OK
primeNotificationsEndpoint(expectedStatus)

Expand All @@ -66,7 +66,7 @@ class NotificationCallbackConnectorISpec extends AnyWordSpec with Matchers with

"not send a status update when notification URL is absent" in new Setup {
val message: OutboundSoapMessage = RetryingOutboundSoapMessage(
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", now, now, httpStatus, None)
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", "some url", now, now, httpStatus, None)

val result: Option[Int] = await(underTest.sendNotification(message))

Expand All @@ -75,7 +75,7 @@ class NotificationCallbackConnectorISpec extends AnyWordSpec with Matchers with

"successfully send a status update with a body to the caller's notification URL" in new Setup {
val message: OutboundSoapMessage = RetryingOutboundSoapMessage(
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", now, now, httpStatus, Some(wireMockBaseUrlAsString))
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", "some url", now, now, httpStatus, Some(wireMockBaseUrlAsString))
val expectedStatus: Int = OK
val expectedNotificationBody: String = Json.toJson(SoapMessageStatus.fromOutboundSoapMessage(message)).toString()
primeNotificationsEndpoint(expectedStatus)
Expand All @@ -86,7 +86,7 @@ class NotificationCallbackConnectorISpec extends AnyWordSpec with Matchers with

"set the Content-Type header to application/json" in new Setup {
val message: OutboundSoapMessage = RetryingOutboundSoapMessage(
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", now, now, httpStatus, Some(wireMockBaseUrlAsString))
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", "some url", now, now, httpStatus, Some(wireMockBaseUrlAsString))
val expectedStatus: Int = OK
primeNotificationsEndpoint(expectedStatus)

Expand All @@ -96,7 +96,7 @@ class NotificationCallbackConnectorISpec extends AnyWordSpec with Matchers with

"handle failed requests to the notification URL" in new Setup {
val message: OutboundSoapMessage = RetryingOutboundSoapMessage(
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", now, now, httpStatus, Some(wireMockBaseUrlAsString))
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", "some url", now, now, httpStatus, Some(wireMockBaseUrlAsString))
val expectedStatus: Int = INTERNAL_SERVER_ERROR
primeNotificationsEndpoint(expectedStatus)

Expand All @@ -107,7 +107,7 @@ class NotificationCallbackConnectorISpec extends AnyWordSpec with Matchers with

"recover from exceptions" in new Setup {
val message: OutboundSoapMessage = RetryingOutboundSoapMessage(
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", now, now, httpStatus, Some("https://invalidUrl"))
globalId, messageId, "<Envelope><Body>foobar</Body></Envelope>", "some url", now, now, httpStatus, Some("https://invalidUrl"))

val result: Option[Int] = await(underTest.sendNotification(message))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import play.api.Application
import play.api.http.HeaderNames.CONTENT_TYPE
import play.api.inject.guice.GuiceApplicationBuilder
import play.api.test.Helpers._
import uk.gov.hmrc.apiplatformoutboundsoap.models.SoapRequest
import uk.gov.hmrc.apiplatformoutboundsoap.support.{Ccn2Service, WireMockSupport}

class OutboundConnectorISpec extends AnyWordSpec with Matchers with GuiceOneAppPerSuite with WireMockSupport with Ccn2Service {
Expand All @@ -31,21 +32,20 @@ class OutboundConnectorISpec extends AnyWordSpec with Matchers with GuiceOneAppP
protected def appBuilder: GuiceApplicationBuilder =
new GuiceApplicationBuilder()
.configure(
"metrics.enabled" -> false,
"auditing.enabled" -> false,
"ccn2Url" -> wireMockBaseUrlAsString
"metrics.enabled" -> false,
"auditing.enabled" -> false
)

trait Setup {
val underTest: OutboundConnector = app.injector.instanceOf[OutboundConnector]
}

"postMessage" should {
val message = "<Envelope><Body>foobar</Body></Envelope>"
val message = SoapRequest("<Envelope><Body>foobar</Body></Envelope>", wireMockBaseUrlAsString)

"return successful statuses returned by the CCN2 service" in new Setup {
val expectedStatus: Int = OK
primeCcn2Endpoint(message, expectedStatus)
primeCcn2Endpoint(message.soapEnvelope, expectedStatus)

val result: Int = await(underTest.postMessage(message))

Expand All @@ -54,23 +54,23 @@ class OutboundConnectorISpec extends AnyWordSpec with Matchers with GuiceOneAppP

"return error statuses returned by the CCN2 service" in new Setup {
val expectedStatus: Int = INTERNAL_SERVER_ERROR
primeCcn2Endpoint(message, expectedStatus)
primeCcn2Endpoint(message.soapEnvelope, expectedStatus)

val result: Int = await(underTest.postMessage(message))

result shouldBe expectedStatus
}

"send the given message to the CCN2 service" in new Setup {
primeCcn2Endpoint(message, OK)
primeCcn2Endpoint(message.soapEnvelope, OK)

await(underTest.postMessage(message))

verifyRequestBody(message)
verifyRequestBody(message.soapEnvelope)
}

"send the right SOAP content type header" in new Setup {
primeCcn2Endpoint(message, OK)
primeCcn2Endpoint(message.soapEnvelope, OK)

await(underTest.postMessage(message))

Expand Down
Loading

0 comments on commit c77a210

Please sign in to comment.