From 827398eec9cd1c1fb05f369896cca0a940e78079 Mon Sep 17 00:00:00 2001 From: Kyle Florence Date: Sun, 17 Jan 2021 14:04:53 -0600 Subject: [PATCH] Fixes #190: allow control over when stub server starts and stops. --- .../SingletonStubProviderClientSpec.scala | 99 +++++++++++++++++++ ...la => StubPerTestProviderClientSpec.scala} | 3 +- .../com/itv/scalapact/ScalaPactMock.scala | 60 +++++++---- .../model/ScalaPactDescription.scala | 28 +++++- 4 files changed, 164 insertions(+), 26 deletions(-) create mode 100644 example/consumer/src/test/scala/com/example/consumer/SingletonStubProviderClientSpec.scala rename example/consumer/src/test/scala/com/example/consumer/{ProviderClientSpec.scala => StubPerTestProviderClientSpec.scala} (96%) diff --git a/example/consumer/src/test/scala/com/example/consumer/SingletonStubProviderClientSpec.scala b/example/consumer/src/test/scala/com/example/consumer/SingletonStubProviderClientSpec.scala new file mode 100644 index 000000000..432f87a30 --- /dev/null +++ b/example/consumer/src/test/scala/com/example/consumer/SingletonStubProviderClientSpec.scala @@ -0,0 +1,99 @@ +package com.example.consumer + +import com.itv.scalapact.{ScalaPactMockConfig, ScalaPactMockServer} +import com.itv.scalapact.model.ScalaPactDescription +import org.json4s.DefaultFormats +import org.json4s.native.Serialization._ +import org.scalatest.{BeforeAndAfterAll, FunSpec, Matchers} + +/** Stands up the stub service with all stubs prior to running tests and shuts it down afterwards. */ +class SingletonStubProviderClientSpec extends FunSpec with Matchers with BeforeAndAfterAll { + + // The import contains two things: + // 1. The consumer test DSL/Builder + // 2. Helper implicits, for instance, values will automatically be converted + // to Option types where the DSL requires it. + import com.itv.scalapact.ScalaPactForger._ + + // Import the json and http libraries specified in the build.sbt file + import com.itv.scalapact.circe13._ + import com.itv.scalapact.http4s21._ + + implicit val formats: DefaultFormats.type = DefaultFormats + + val CONSUMER = "scala-pact-consumer" + val PROVIDER = "scala-pact-provider" + + val people = List("Bob", "Fred", "Harry") + + val body: String = write( + Results( + count = 3, + results = people + ) + ) + + // Forge all pacts up front + val pact: ScalaPactDescription = forgePact + .between(CONSUMER) + .and(PROVIDER) + .addInteraction( + interaction + .description("Fetching results") + .given("Results: Bob, Fred, Harry") + .uponReceiving("/results") + .willRespondWith(200, Map("Pact" -> "modifiedRequest"), body) + ) + .addInteraction( + interaction + .description("Fetching least secure auth token ever") + .uponReceiving( + method = GET, + path = "/auth_token", + query = None, + headers = Map("Accept" -> "application/json", "Name" -> "Bob"), + body = None, + matchingRules = // When stubbing (during this test or externally), we don't mind + // what the name is, as long as it only contains letters. + headerRegexRule("Name", "^([a-zA-Z]+)$") + ) + .willRespondWith( + status = 202, + headers = Map("Content-Type" -> "application/json; charset=UTF-8"), + body = Some("""{"token":"abcABC123"}"""), + matchingRules = // When verifying externally, we don't mind what is in the token + // as long as it contains a token field with an alphanumeric + // value + bodyRegexRule("token", "^([a-zA-Z0-9]+)$") + ) + ) + + lazy val server: ScalaPactMockServer = pact.startServer() + lazy val config: ScalaPactMockConfig = server.config + + override def beforeAll(): Unit = { + // Initialize the Pact stub server prior to tests executing. + val _ = server + () + } + + override def afterAll(): Unit = { + // Shut down the stub server when tests are finished. + server.stop() + } + + describe("Connecting to the Provider service") { + it("should be able to fetch results") { + val results = ProviderClient.fetchResults(config.baseUrl) + results.isDefined shouldEqual true + results.get.count shouldEqual 3 + results.get.results.forall(p => people.contains(p)) shouldEqual true + } + + it("should be able to get an auth token") { + val token = ProviderClient.fetchAuthToken(config.host, config.port, "Sally") + token.isDefined shouldEqual true + token.get.token shouldEqual "abcABC123" + } + } +} diff --git a/example/consumer/src/test/scala/com/example/consumer/ProviderClientSpec.scala b/example/consumer/src/test/scala/com/example/consumer/StubPerTestProviderClientSpec.scala similarity index 96% rename from example/consumer/src/test/scala/com/example/consumer/ProviderClientSpec.scala rename to example/consumer/src/test/scala/com/example/consumer/StubPerTestProviderClientSpec.scala index 48b62249b..6aa7902a6 100644 --- a/example/consumer/src/test/scala/com/example/consumer/ProviderClientSpec.scala +++ b/example/consumer/src/test/scala/com/example/consumer/StubPerTestProviderClientSpec.scala @@ -4,7 +4,8 @@ import org.json4s.DefaultFormats import org.json4s.native.Serialization._ import org.scalatest.{FunSpec, Matchers} -class ProviderClientSpec extends FunSpec with Matchers { +/** Stands up a stub service per test case. */ +class StubPerTestProviderClientSpec extends FunSpec with Matchers { // The import contains two things: // 1. The consumer test DSL/Builder diff --git a/scalapact-scalatest/src/main/scala/com/itv/scalapact/ScalaPactMock.scala b/scalapact-scalatest/src/main/scala/com/itv/scalapact/ScalaPactMock.scala index cb6eaaa7a..77a6c48a2 100644 --- a/scalapact-scalatest/src/main/scala/com/itv/scalapact/ScalaPactMock.scala +++ b/scalapact-scalatest/src/main/scala/com/itv/scalapact/ScalaPactMock.scala @@ -20,16 +20,16 @@ private[scalapact] object ScalaPactMock { test(config) } - def runConsumerIntegrationTest[A]( - strict: Boolean - )(pactDescription: ScalaPactDescriptionFinal)(test: ScalaPactMockConfig => A)(implicit - sslContextMap: SslContextMap, + def startServer( + strict: Boolean, + pactDescription: ScalaPactDescriptionFinal + )(implicit + httpClient: IScalaPactHttpClient, pactReader: IPactReader, pactWriter: IPactWriter, - httpClient: IScalaPactHttpClient, - pactStubber: IPactStubber - ): A = { - + pactStubber: IPactStubber, + sslContextMap: SslContextMap + ): ScalaPactMockServer = { val interactionManager: InteractionManager = new InteractionManager val protocol = pactDescription.serverSslContextName.fold("http")(_ => "https") @@ -64,23 +64,34 @@ private[scalapact] object ScalaPactMock { PactLogger.debug("> ScalaPact stub running at: " + mockConfig.baseUrl) - waitForServerThenTest(server, mockConfig, test, pactDescription) + val mockServer = new ScalaPactMockServer(server, mockConfig) + waitForServer(mockConfig, pactDescription.serverSslContextName) + mockServer + } + + def runConsumerIntegrationTest[A]( + strict: Boolean + )(pactDescription: ScalaPactDescriptionFinal)(test: ScalaPactMockConfig => A)(implicit + sslContextMap: SslContextMap, + pactReader: IPactReader, + pactWriter: IPactWriter, + httpClient: IScalaPactHttpClient, + pactStubber: IPactStubber + ): A = { + val server = startServer(strict, pactDescription) + val result = configuredTestRunner(pactDescription)(server.config)(test) + server.stop() + result } - private def waitForServerThenTest[A]( - server: IPactStubber, + private def waitForServer( mockConfig: ScalaPactMockConfig, - test: ScalaPactMockConfig => A, - pactDescription: ScalaPactDescriptionFinal - )(implicit pactWriter: IPactWriter, httpClient: IScalaPactHttpClient): A = { + serverSslContextName: Option[String] + )(implicit httpClient: IScalaPactHttpClient): Unit = { @scala.annotation.tailrec - def rec(attemptsRemaining: Int, intervalMillis: Int): A = - if (isStubReady(mockConfig, pactDescription.serverSslContextName)) { - val result = configuredTestRunner(pactDescription)(mockConfig)(test) - - server.shutdown() - - result + def rec(attemptsRemaining: Int, intervalMillis: Int): Unit = + if (isStubReady(mockConfig, serverSslContextName)) { + PactLogger.debug("Stub server is ready.") } else if (attemptsRemaining == 0) { throw new Exception("Could not connect to stub at: " + mockConfig.baseUrl) } else { @@ -118,3 +129,10 @@ private[scalapact] object ScalaPactMock { case class ScalaPactMockConfig(protocol: String, host: String, port: Int, outputPath: String) { val baseUrl: String = protocol + "://" + host + ":" + port.toString } + +class ScalaPactMockServer( + underlying: IPactStubber, + val config: ScalaPactMockConfig +) { + def stop(): Unit = underlying.shutdown() +} diff --git a/scalapact-scalatest/src/main/scala/com/itv/scalapact/model/ScalaPactDescription.scala b/scalapact-scalatest/src/main/scala/com/itv/scalapact/model/ScalaPactDescription.scala index 45446c569..2fb00b805 100644 --- a/scalapact-scalatest/src/main/scala/com/itv/scalapact/model/ScalaPactDescription.scala +++ b/scalapact-scalatest/src/main/scala/com/itv/scalapact/model/ScalaPactDescription.scala @@ -1,7 +1,7 @@ package com.itv.scalapact.model import com.itv.scalapact.shared.IPactStubber -import com.itv.scalapact.{ScalaPactContractWriter, ScalaPactMock, ScalaPactMockConfig} +import com.itv.scalapact.{ScalaPactContractWriter, ScalaPactMock, ScalaPactMockConfig, ScalaPactMockServer} import com.itv.scalapact.shared.utils.Maps._ import com.itv.scalapact.shared.http.{IScalaPactHttpClient, IScalaPactHttpClientBuilder, SslContextMap} import com.itv.scalapact.shared.json.{IPactReader, IPactWriter} @@ -37,9 +37,29 @@ class ScalaPactDescription( ): A = { implicit val client: IScalaPactHttpClient = httpClientBuilder.build(2.seconds, sslContextName, 1) - ScalaPactMock.runConsumerIntegrationTest(strict)( - finalise - )(test) + ScalaPactMock.runConsumerIntegrationTest(strict)(finalise)(test) + } + + /** Starts the `ScalaPactMockServer`, which tests can then be run against. It is important that the server be + * shutdown when no longer needed by invoking `stop()`. + */ + def startServer()(implicit + httpClientBuilder: IScalaPactHttpClientBuilder, + options: ScalaPactOptions, + pactReader: IPactReader, + pactWriter: IPactWriter, + pactStubber: IPactStubber + ): ScalaPactMockServer = { + implicit val client: IScalaPactHttpClient = + httpClientBuilder.build(2.seconds, sslContextName, 1) + val pactDescriptionFinal = finalise(options) + val server = ScalaPactMock.startServer(strict, pactDescriptionFinal) + if (pactDescriptionFinal.options.writePactFiles) { + ScalaPactContractWriter.writePactContracts(server.config.outputPath)(pactWriter)( + pactDescriptionFinal.withHeaderForSsl + ) + } + server } /** Writes pacts described by this ScalaPactDescription to file without running any consumer tests