Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ID-377 Setup pact for consumer contract testing. #7123

Merged
merged 12 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,11 @@ lazy val `cromwell-drs-localizer` = project
.dependsOn(common)
.dependsOn(`cloud-nio-impl-drs` % "test->test")

lazy val pact4s = project.in(file("pact4s"))
.settings(pact4sSettings)
.dependsOn(services)
.disablePlugins(sbtassembly.AssemblyPlugin)

lazy val server = project
.withExecutableSettings("cromwell", serverDependencies)
.dependsOn(engine)
Expand Down Expand Up @@ -419,4 +424,5 @@ lazy val root = (project in file("."))
.aggregate(wes2cromwell)
.aggregate(wom)
.aggregate(womtool)
.aggregate(pact4s)
.withAggregateSettings()
51 changes: 51 additions & 0 deletions pact4s/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
c# pact4s [Under construction]

pact4s is used for contract testing.

# Dependencies

```scala
val pact4sDependencies = Seq(
pact4sScalaTest,
pact4sCirce,
http4sEmberClient,
http4sDsl,
http4sEmberServer,
http4sCirce,
circeCore,
typelevelCat,
scalaTest
)

lazy val pact4s = project.in(file("pact4s"))
.settings(pact4sSettings)
.dependsOn(http % "test->test;compile->compile")
```

## Building and running contract tests
Clone the repo.
```
$ git clone https://github.com/broadinstitute/cromwell.git
$ cd cromwell
```

If you are already using OpenJDK 11, run the following command.
```
$ sbt "project pact4s" clean test
```

Otherwise, you can run the command inside a docker container with OpenJDK 11 installed.
This is especially useful when automating contract tests in a GitHub Action runner which does not guarantee the correct OpenJDK version.
```
docker run --rm -v $PWD:/working \
-v jar-cache:/root/.ivy \
-v jar-cache:/root/.ivy2 \
-w /working \
sbtscala/scala-sbt:openjdk-11.0.16_1.8.1_2.13.10 \
sbt "project pact4s" clean test
```

The generated contracts can be found in the `./target/pacts` folder
- `cromwell-consumer-drshub-provider.json`
- `cromwell-consumer-fake-provider.json`

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.broadinstitute.dsde.workbench.cromwell.consumer

import cats.effect.Concurrent
import cats.syntax.all._
import org.http4s.Credentials.Token
import org.http4s._
import org.http4s.client.Client


trait DrsHubClient[F[_]] {
def fetchSystemStatus(): F[Boolean]
}

/*
This class represents the consumer (Cromwell) view of the DrsHub provider that implements the following endpoints:
- GET /status
*/
class DrsHubClientImpl[F[_]: Concurrent](client: Client[F], baseUrl: Uri, bearer: Token) extends DrsHubClient[F] {
override def fetchSystemStatus(): F[Boolean] = {
val request = Request[F](uri = baseUrl / "status").withHeaders(
org.http4s.headers.Accept(MediaType.application.json)
)
client.run(request).use { resp =>
resp.status match {
case Status.Ok => true.pure[F]
case Status.InternalServerError => false.pure[F]
case _ => UnknownError.raiseError
}
}
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package org.broadinstitute.dsde.workbench.cromwell.consumer

import au.com.dius.pact.consumer.dsl.{DslPart, PactDslResponse, PactDslWithProvider}
import org.broadinstitute.dsde.workbench.model.WorkbenchEmail
import org.http4s.Credentials.Token
import org.http4s.{AuthScheme, Credentials}
import pact4s.algebras.PactBodyJsonEncoder
case object UnknownError extends Exception

object AuthHelper {
def mockBearerHeader(workbenchEmail: WorkbenchEmail) = s"Bearer TokenFor$workbenchEmail"
def mockAuthToken(workbenchEmail: WorkbenchEmail): Token =
Credentials.Token(AuthScheme.Bearer, s"TokenFor$workbenchEmail")
}

object PactHelper {
def buildInteraction(builder: PactDslResponse,
state: String,
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
status: Int,
responseHeaders: Seq[(String, String)],
body: DslPart
): PactDslResponse =
builder
.`given`(state)
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.willRespondWith()
.status(status)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(responseHeaders.toMap).asJava)
.body(body)

def buildInteraction(builder: PactDslResponse,
state: String,
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
status: Int
): PactDslResponse =
builder
.`given`(state)
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.willRespondWith()
.status(status)

def buildInteraction(builder: PactDslResponse,
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
status: Int
): PactDslResponse =
builder
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.willRespondWith()
.status(status)

def buildInteraction(builder: PactDslWithProvider,
state: String,
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
status: Int,
responseHeaders: Seq[(String, String)],
body: DslPart
): PactDslResponse =
builder
.`given`(state)
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.willRespondWith()
.status(status)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(responseHeaders.toMap).asJava)
.body(body)

def buildInteraction(builder: PactDslWithProvider,
state: String,
stateParams: Map[String, Any],
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
status: Int,
responseHeaders: Seq[(String, String)],
body: DslPart
): PactDslResponse =
builder
.`given`(state, scala.jdk.CollectionConverters.MapHasAsJava(stateParams).asJava)
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.willRespondWith()
.status(status)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(responseHeaders.toMap).asJava)
.body(body)

def buildInteraction(builder: PactDslResponse,
state: String,
stateParams: Map[String, Any],
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
status: Int,
responseHeaders: Seq[(String, String)],
body: DslPart
): PactDslResponse =
builder
.`given`(state, scala.jdk.CollectionConverters.MapHasAsJava(stateParams).asJava)
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.willRespondWith()
.status(status)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(responseHeaders.toMap).asJava)
.body(body)

def buildInteraction(builder: PactDslResponse,
state: String,
stateParams: Map[String, Any],
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
status: Int,
responseHeaders: Seq[(String, String)],
): PactDslResponse =
builder
.`given`(state, scala.jdk.CollectionConverters.MapHasAsJava(stateParams).asJava)
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.willRespondWith()
.status(status)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(responseHeaders.toMap).asJava)


def buildInteraction[A](builder: PactDslWithProvider,
state: String,
stateParams: Map[String, Any],
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
status: Int,
responseHeaders: Seq[(String, String)],
body: A
)(implicit ev: PactBodyJsonEncoder[A]): PactDslResponse =
builder
.`given`(state, scala.jdk.CollectionConverters.MapHasAsJava(stateParams).asJava)
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.willRespondWith()
.status(status)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(responseHeaders.toMap).asJava)
.body(ev.toJsonString(body))

def buildInteraction[A](builder: PactDslResponse,
state: String,
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
requestBody: A,
status: Int
)(implicit ev: PactBodyJsonEncoder[A]): PactDslResponse =
builder
.`given`(state)
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.body(ev.toJsonString(requestBody))
.willRespondWith()
.status(status)

def buildInteraction(builder: PactDslWithProvider,
state: String,
stateParams: Map[String, Any],
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
status: Int,
responseHeaders: Seq[(String, String)],
): PactDslResponse =
builder
.`given`(state, scala.jdk.CollectionConverters.MapHasAsJava(stateParams).asJava)
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.willRespondWith()
.status(status)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(responseHeaders.toMap).asJava)


def buildInteraction[A](builder: PactDslResponse,
state: String,
stateParams: Map[String, Any],
uponReceiving: String,
method: String,
path: String,
requestHeaders: Seq[(String, String)],
requestBody: A,
status: Int
)(implicit ev: PactBodyJsonEncoder[A]): PactDslResponse =
builder
.`given`(state, scala.jdk.CollectionConverters.MapHasAsJava(stateParams).asJava)
.uponReceiving(uponReceiving)
.method(method)
.path(path)
.headers(scala.jdk.CollectionConverters.MapHasAsJava(requestHeaders.toMap).asJava)
.body(ev.toJsonString(requestBody))
.willRespondWith()
.status(status)
}
14 changes: 14 additions & 0 deletions pact4s/src/test/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<logger name="io.github.jbwheatley.pact4s.Pact4sLogger" level="INFO" />
<logger name="au.com.dius.pact.consumer" level="DEBUG"/>
<logger name="au.com.dius.pact.provider" level="DEBUG"/>

<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package cromwell.filesystems.blob
package org.broadinstitute.dsde.workbench.cromwell.consumer

import au.com.dius.pact.consumer.{ConsumerPactBuilder, PactTestExecutionContext}
import au.com.dius.pact.core.model.RequestResponsePact
Expand Down
Loading