diff --git a/pact-jvm-server/build.gradle b/pact-jvm-server/build.gradle index b05807919..2015cc75d 100644 --- a/pact-jvm-server/build.gradle +++ b/pact-jvm-server/build.gradle @@ -22,8 +22,9 @@ dependencies { exclude module: 'netty-transport-native-epoll' } implementation 'org.apache.commons:commons-io:1.3.2' - implementation 'org.apache.tika:tika-core' implementation 'org.apache.commons:commons-lang3' + implementation 'org.apache.commons:commons-text' + implementation 'org.apache.tika:tika-core' testImplementation 'org.apache.groovy:groovy' testImplementation 'org.apache.groovy:groovy-json' diff --git a/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/PactSession.kt b/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/PactSession.kt new file mode 100644 index 000000000..d9e01cd4b --- /dev/null +++ b/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/PactSession.kt @@ -0,0 +1,67 @@ +package au.com.dius.pact.server + +import au.com.dius.pact.core.matchers.FullRequestMatch +import au.com.dius.pact.core.matchers.PartialRequestMatch +import au.com.dius.pact.core.matchers.RequestMatching +import au.com.dius.pact.core.matchers.RequestMismatch +import au.com.dius.pact.core.model.IResponse +import au.com.dius.pact.core.model.Interaction +import au.com.dius.pact.core.model.OptionalBody +import au.com.dius.pact.core.model.Pact +import au.com.dius.pact.core.model.Request +import au.com.dius.pact.core.model.Response +import org.apache.commons.text.StringEscapeUtils + +data class PactSession( + val expected: Pact?, + val results: PactSessionResults +) { + fun receiveRequest(req: Request): Pair { + val invalidResponse = invalidRequest(req) + + return if (expected != null) { + val matcher = RequestMatching(expected) + when (val result = matcher.matchInteraction(req)) { + is FullRequestMatch -> + (result.interaction.asSynchronousRequestResponse()!!.response to recordMatched(result.interaction)) + + is PartialRequestMatch -> + (invalidResponse to recordAlmostMatched(result)) + + is RequestMismatch -> + (invalidResponse to recordUnexpected(req)) + } + } else { + invalidResponse to this + } + } + + fun recordUnexpected(req: Request) = this.copy(results = results.addUnexpected(req)) + + fun recordAlmostMatched(partial: PartialRequestMatch) = this.copy(results = results.addAlmostMatched(partial)) + + fun recordMatched(interaction: Interaction) = this.copy(results = results.addMatched(interaction)) + + fun remainingResults() = if (expected != null) + results.addMissing((expected.interactions - results.matched.toSet()).asIterable()) + else results + + companion object { + val CrossSiteHeaders = mapOf("Access-Control-Allow-Origin" to listOf("*")) + @JvmStatic + val empty = PactSession(null, PactSessionResults.empty) + + @JvmStatic + fun forPact(pact: Pact) = PactSession(pact, PactSessionResults.empty) + + @JvmStatic + fun invalidRequest(req: Request): IResponse { + val headers = CrossSiteHeaders + mapOf( + "Content-Type" to listOf("application/json"), + "X-Pact-Unexpected-Request" to listOf("1") + ) + val body = "{ \"error\": \"Unexpected request : " + StringEscapeUtils.escapeJson(req.toString()) + "\" }" + return Response(500, headers.toMutableMap(), OptionalBody.body(body.toByteArray())) + } + } +} diff --git a/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/PactSessionResults.kt b/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/PactSessionResults.kt index 27d0e6895..c9d187ab0 100644 --- a/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/PactSessionResults.kt +++ b/pact-jvm-server/src/main/kotlin/au/com/dius/pact/server/PactSessionResults.kt @@ -3,7 +3,6 @@ package au.com.dius.pact.server import au.com.dius.pact.core.matchers.PartialRequestMatch import au.com.dius.pact.core.model.Interaction import au.com.dius.pact.core.model.Request -import scala.collection.JavaConverters.asJavaIterable data class PactSessionResults( val matched: List, @@ -13,7 +12,7 @@ data class PactSessionResults( ) { fun addMatched(inter: Interaction) = copy(matched = listOf(inter) + matched) fun addUnexpected(request: Request) = copy(unexpected = listOf(request) + unexpected) - fun addMissing(inters: scala.collection.Iterable) = copy(missing = asJavaIterable(inters).toList() + missing) + fun addMissing(inters: Iterable) = copy(missing = inters.toList() + missing) fun addAlmostMatched(partial: PartialRequestMatch) = copy(almostMatched = listOf(partial) + almostMatched) fun allMatched(): Boolean = missing.isEmpty() && unexpected.isEmpty() diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Conversions.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Conversions.scala index d971c9303..5d4de6c2a 100644 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Conversions.scala +++ b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Conversions.scala @@ -2,8 +2,7 @@ package au.com.dius.pact.server import java.net.URI import java.util.zip.GZIPInputStream - -import au.com.dius.pact.core.model.{OptionalBody, ContentType, Request, Response} +import au.com.dius.pact.core.model.{ContentType, IResponse, OptionalBody, Request} import com.typesafe.scalalogging.StrictLogging import io.netty.handler.codec.http.{HttpResponse => NHttpResponse} import unfiltered.netty.ReceivedMessage @@ -22,7 +21,7 @@ object Conversions extends StrictLogging { } } - def pactToUnfilteredResponse(response: Response): ResponseFunction[NHttpResponse] = { + def pactToUnfilteredResponse(response: IResponse): ResponseFunction[NHttpResponse] = { val headers = response.getHeaders if (response.getBody.isPresent) { Status(response.getStatus) ~> Headers(headers) ~> ResponseString(response.getBody.valueAsString) diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/MockProvider.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/MockProvider.scala index 98f61ffdd..5d54e0836 100644 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/MockProvider.scala +++ b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/MockProvider.scala @@ -1,7 +1,7 @@ package au.com.dius.pact.server import au.com.dius.pact.consumer.model.{MockHttpsKeystoreProviderConfig, MockHttpsProviderConfig, MockProviderConfig} -import au.com.dius.pact.core.model.{PactSpecVersion, Request, Response, Pact => PactModel} +import au.com.dius.pact.core.model.{IResponse, PactSpecVersion, Request, Response, Pact => PactModel} import com.typesafe.scalalogging.StrictLogging import scala.util.Try @@ -32,7 +32,7 @@ object DefaultMockProvider { // TODO: eliminate horrid state mutation and synchronisation. Reactive stuff to the rescue? abstract class StatefulMockProvider extends MockProvider with StrictLogging { - private var sessionVar = PactSession.empty + private var sessionVar = PactSession.getEmpty private var pactVar: Option[PactModel] = None private def waitForRequestsToFinish() = Thread.sleep(100) @@ -69,9 +69,11 @@ abstract class StatefulMockProvider extends MockProvider with StrictLogging { } } - final def handleRequest(req: Request): Response = synchronized { + final def handleRequest(req: Request): IResponse = synchronized { logger.debug("Received request: " + req) - val (response, newSession) = session.receiveRequest(req) + val result = session.receiveRequest(req) + val response = result.getFirst + val newSession = result.getSecond logger.debug("Generating response: " + response) sessionVar = newSession response diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/PactSession.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/PactSession.scala deleted file mode 100644 index 59b49543d..000000000 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/PactSession.scala +++ /dev/null @@ -1,53 +0,0 @@ -package au.com.dius.pact.server - -import au.com.dius.pact.core.matchers.{FullRequestMatch, PartialRequestMatch, RequestMatching, RequestMismatch} -import au.com.dius.pact.core.model.{Interaction, OptionalBody, Request, RequestResponseInteraction, Response, Pact => PactModel} -import org.apache.commons.lang3.StringEscapeUtils - -object PactSession { - val empty = PactSession(None, PactSessionResults.getEmpty) - - def forPact(pact: PactModel) = PactSession(Some(pact), PactSessionResults.getEmpty) -} - -case class PactSession(expected: Option[PactModel], results: PactSessionResults) { - import scala.collection.JavaConverters._ - - val CrossSiteHeaders = Map[String, java.util.List[String]]("Access-Control-Allow-Origin" -> List("*").asJava) - - def invalidRequest(req: Request) = { - val headers: Map[String, java.util.List[String]] = CrossSiteHeaders ++ Map("Content-Type" -> List("application/json").asJava, - "X-Pact-Unexpected-Request" -> List("1").asJava) - val body = "{ \"error\": \"Unexpected request : " + StringEscapeUtils.escapeJson(req.toString) + "\" }" - new Response(500, headers.asJava, OptionalBody.body(body.getBytes)) - } - - def receiveRequest(req: Request): (Response, PactSession) = { - val invalidResponse = invalidRequest(req) - - val matcher = new RequestMatching(expected.get) - matcher.matchInteraction(req) match { - case frm: FullRequestMatch => - (frm.getInteraction.asInstanceOf[RequestResponseInteraction].getResponse, recordMatched(frm.getInteraction)) - - case p: PartialRequestMatch => - (invalidResponse, recordAlmostMatched(p)) - - case _: RequestMismatch => - (invalidResponse, recordUnexpected(req)) - } - } - - def recordUnexpected(req: Request): PactSession = - copy(results = results addUnexpected req) - - def recordAlmostMatched(partial: PartialRequestMatch): PactSession = - copy(results = results addAlmostMatched partial) - - def recordMatched(interaction: Interaction): PactSession = - copy(results = results addMatched interaction) - - def withTheRestMissing: PactSession = PactSession(None, remainingResults) - - def remainingResults: PactSessionResults = results.addMissing(expected.get.getInteractions.asScala diff results.getMatched.asScala) -} diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/RequestRouter.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/RequestRouter.scala index 40d4bad58..275031fb2 100644 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/RequestRouter.scala +++ b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/RequestRouter.scala @@ -13,7 +13,7 @@ object RequestRouter { pact <- oldState.get(k) } yield pact).headOption - def handlePactRequest(request: Request, oldState: ServerState): Option[Response] = + def handlePactRequest(request: Request, oldState: ServerState): Option[IResponse] = for { pact <- matchPath(request, oldState) } yield pact.handleRequest(request) @@ -23,7 +23,7 @@ object RequestRouter { val EMPTY_MAP: util.Map[String, util.List[String]] = Map[String, util.List[String]]().asJava - def pactDispatch(request: Request, oldState: ServerState): Response = + def pactDispatch(request: Request, oldState: ServerState): IResponse = handlePactRequest(request, oldState) getOrElse new Response(404, EMPTY_MAP, OptionalBody.body(state404(request, oldState).getBytes)) diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Server.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Server.scala index 119379cec..b0a9e0b54 100644 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Server.scala +++ b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/Server.scala @@ -1,6 +1,6 @@ package au.com.dius.pact.server -import au.com.dius.pact.core.model.{OptionalBody, Response} +import au.com.dius.pact.core.model.{IResponse, OptionalBody, Response} import ch.qos.logback.classic.Level import org.slf4j.{Logger, LoggerFactory} @@ -16,7 +16,7 @@ object ListServers { } } -case class Result(response: Response, newState: ServerState) +case class Result(response: IResponse, newState: ServerState) object Server extends App { diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredHttpsKeystoreMockProvider.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredHttpsKeystoreMockProvider.scala index e8d7e0772..f7d188824 100644 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredHttpsKeystoreMockProvider.scala +++ b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredHttpsKeystoreMockProvider.scala @@ -3,7 +3,7 @@ package au.com.dius.pact.server import _root_.unfiltered.netty.{SslEngineProvider, cycle => unettyc} import _root_.unfiltered.{netty => unetty, request => ureq, response => uresp} import au.com.dius.pact.consumer.model.MockHttpsKeystoreProviderConfig -import au.com.dius.pact.core.model.{Request, Response} +import au.com.dius.pact.core.model.{IResponse, Request} import io.netty.channel.ChannelHandler.Sharable import io.netty.handler.codec.{http => netty} @@ -26,7 +26,7 @@ class UnfilteredHttpsKeystoreMockProvider(val config: MockHttpsKeystoreProviderC def convertRequest(nr: UnfilteredRequest): Request = Conversions.unfilteredRequestToPactRequest(nr) - def convertResponse(response: Response): UnfilteredResponse = Conversions.pactToUnfilteredResponse(response) + def convertResponse(response: IResponse): UnfilteredResponse = Conversions.pactToUnfilteredResponse(response) } def start(): Unit = server.start() diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredHttpsMockProvider.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredHttpsMockProvider.scala index 7a6b9cdfa..f0483b78b 100644 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredHttpsMockProvider.scala +++ b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredHttpsMockProvider.scala @@ -3,7 +3,7 @@ package au.com.dius.pact.server import _root_.unfiltered.netty.{SslContextProvider, cycle => unettyc} import _root_.unfiltered.{netty => unetty, request => ureq, response => uresp} import au.com.dius.pact.consumer.model.MockHttpsProviderConfig -import au.com.dius.pact.core.model.{Request, Response} +import au.com.dius.pact.core.model.{IResponse, Request} import io.netty.channel.ChannelHandler.Sharable import io.netty.handler.codec.{http => netty} import io.netty.handler.ssl.util.SelfSignedCertificate @@ -27,7 +27,7 @@ class UnfilteredHttpsMockProvider(val config: MockHttpsProviderConfig) extends S def convertRequest(nr: UnfilteredRequest): Request = Conversions.unfilteredRequestToPactRequest(nr) - def convertResponse(response: Response): UnfilteredResponse = Conversions.pactToUnfilteredResponse(response) + def convertResponse(response: IResponse): UnfilteredResponse = Conversions.pactToUnfilteredResponse(response) } def start(): Unit = server.start() diff --git a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredMockProvider.scala b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredMockProvider.scala index 15042e241..bcc64f3e8 100644 --- a/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredMockProvider.scala +++ b/pact-jvm-server/src/main/scala/au/com/dius/pact/server/UnfilteredMockProvider.scala @@ -3,7 +3,7 @@ package au.com.dius.pact.server import _root_.unfiltered.netty.{cycle => unettyc} import _root_.unfiltered.{netty => unetty, request => ureq, response => uresp} import au.com.dius.pact.consumer.model.MockProviderConfig -import au.com.dius.pact.core.model.{Request, Response} +import au.com.dius.pact.core.model.{IResponse, Request} import io.netty.channel.ChannelHandler.Sharable import io.netty.handler.codec.{http => netty} @@ -24,7 +24,7 @@ class UnfilteredMockProvider(val config: MockProviderConfig) extends StatefulMoc def convertRequest(nr: UnfilteredRequest): Request = Conversions.unfilteredRequestToPactRequest(nr) - def convertResponse(response: Response): UnfilteredResponse = Conversions.pactToUnfilteredResponse(response) + def convertResponse(response: IResponse): UnfilteredResponse = Conversions.pactToUnfilteredResponse(response) } def start(): Unit = server.start() diff --git a/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PactSessionResultsSpec.groovy b/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PactSessionResultsSpec.groovy index 7c1d64c70..3dc8ebb74 100644 --- a/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PactSessionResultsSpec.groovy +++ b/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PactSessionResultsSpec.groovy @@ -5,8 +5,6 @@ import au.com.dius.pact.core.model.Request import au.com.dius.pact.core.model.RequestResponseInteraction import spock.lang.Specification -import static scala.collection.JavaConverters.iterableAsScalaIterable - class PactSessionResultsSpec extends Specification { def 'empty state'() { given: @@ -75,8 +73,8 @@ class PactSessionResultsSpec extends Specification { def interaction3 = new RequestResponseInteraction('test3') when: - state = state.addMissing(iterableAsScalaIterable([interaction1])) - state = state.addMissing(iterableAsScalaIterable([interaction2, interaction3])) + state = state.addMissing([interaction1]) + state = state.addMissing([interaction2, interaction3]) then: !state.allMatched() diff --git a/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PactSessionSpec.groovy b/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PactSessionSpec.groovy new file mode 100644 index 000000000..8ba2dca8b --- /dev/null +++ b/pact-jvm-server/src/test/groovy/au/com/dius/pact/server/PactSessionSpec.groovy @@ -0,0 +1,95 @@ +package au.com.dius.pact.server + +import au.com.dius.pact.core.matchers.PartialRequestMatch +import au.com.dius.pact.core.model.Consumer +import au.com.dius.pact.core.model.Provider +import au.com.dius.pact.core.model.Request +import au.com.dius.pact.core.model.RequestResponseInteraction +import au.com.dius.pact.core.model.RequestResponsePact +import spock.lang.Specification + +class PactSessionSpec extends Specification { + def 'invalid request returns JSON response with details about the request'() { + given: + def session = PactSession.empty + + when: + def response = session.invalidRequest(new Request("GET", "/test")) + + then: + response.status == 500 + response.body.valueAsString() == '{ "error": "Unexpected request : \\tmethod: GET\\n\\tpath: \\/test\\n\\tquery: {}\\n\\theaders: {}\\n\\tmatchers: MatchingRules(rules={})\\n\\tgenerators: Generators(categories={})\\n\\tbody: MISSING" }' + } + + def 'receive request records the match against the expected request'() { + given: + def interaction = new RequestResponseInteraction('test', [], new Request('GET', '/test')) + def pact = new RequestResponsePact(new Provider(), new Consumer(), [ interaction ]) + def session = PactSession.forPact(pact) + def request = new Request('GET', '/test') + + when: + def response = session.receiveRequest(request) + + then: + response.first.status == 200 + response.second.results.allMatched() + } + + def 'record unexpected adds an unexpected request to the session'() { + given: + def session = PactSession.empty + def request = new Request('GET', '/test') + + when: + def result = session.recordUnexpected(request) + + then: + !result.results.allMatched() + result.results.unexpected == [ request ] + } + + def 'record almost matched adds a match result to the session'() { + given: + def session = PactSession.empty + def matchResult = new PartialRequestMatch([:]) + + when: + def result = session.recordAlmostMatched(matchResult) + + then: + result.results.allMatched() + result.results.almostMatched == [ matchResult ] + } + + def 'record matched adds the interaction to the session'() { + given: + def session = PactSession.empty + def interaction = new RequestResponseInteraction('test', [], new Request('GET', '/test')) + + when: + def result = session.recordMatched(interaction) + + then: + result.results.allMatched() + result.results.matched == [ interaction ] + } + + def 'remaining results returns any unmatched interactions as missing'() { + given: + def interaction1 = new RequestResponseInteraction('test', [], new Request('GET', '/test')) + def interaction2 = new RequestResponseInteraction('test2', [], new Request('GET', '/test2')) + def interaction3 = new RequestResponseInteraction('test3', [], new Request('GET', '/test3')) + def pact = new RequestResponsePact(new Provider(), new Consumer(), [ interaction1, interaction2, interaction3 ]) + def session = PactSession.forPact(pact) + + when: + session = session.recordMatched(interaction1) + session = session.recordMatched(interaction3) + def result = session.remainingResults() + + then: + !result.allMatched() + result.missing == [ interaction2 ] + } +}