├── .gitignore ├── .travis.yml ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt ├── scalariform.sbt ├── src ├── main │ └── scala │ │ └── tv │ │ └── teads │ │ └── wiremock │ │ └── extension │ │ ├── Calculator.scala │ │ ├── FreeMarkerRenderer.scala │ │ ├── JsonExtractor.scala │ │ └── Randomizer.scala └── test │ └── scala │ └── tv │ └── teads │ └── wiremock │ └── extension │ ├── CalculatorSpec.scala │ ├── CombinationsSpec.scala │ ├── ExtensionSpec.scala │ ├── FreeMarkerRendererSpec.scala │ ├── JsonExtractorSpec.scala │ ├── RandomizerSpec.scala │ └── package.scala └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # SBT 5 | 6 | .cache 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # IDE 16 | 17 | .idea 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | jdk: 3 | - oraclejdk8 4 | script: 5 | - sbt test 6 | - find $HOME/.sbt -name "*.lock" | xargs rm 7 | - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm 8 | 9 | sudo: false 10 | 11 | # Cache settings 12 | cache: 13 | directories: 14 | - $HOME/.ivy2/cache 15 | - $HOME/.sbt/launchers 16 | 17 | # whitelist 18 | branches: 19 | only: 20 | - master 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WireMock Extensions [![Build Status](https://travis-ci.org/teads/wiremock-extensions.svg?branch=master)](https://travis-ci.org/teads/wiremock-extensions) 2 | 3 | wiremock-extensions is a set of extensions for WireMock. 4 | 5 | ## Installation 6 | 7 | For now, wiremock-extensions is not published on Maven Central Repository. 8 | The only way is through the WireMock standalone process. 9 | 10 | 1. [Download WireMock Jar](https://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-standalone/2.8.0/wiremock-standalone-2.8.0.jar) 11 | 2. [Download Extension Jar](https://github.com/teads/wiremock-extensions/releases/download/v0.15/wiremock-extensions_2.11-0.15.jar) 12 | 3. Run 13 | ``` 14 | java -cp "wiremock-standalone-2.8.0.jar:wiremock-extensions_2.11-0.15.jar" \ 15 | com.github.tomakehurst.wiremock.standalone.WireMockServerRunner \ 16 | --extensions tv.teads.wiremock.extension.JsonExtractor,tv.teads.wiremock.extension.Calculator,tv.teads.wiremock.extension.FreeMarkerRenderer,tv.teads.wiremock.extension.Randomizer 17 | ``` 18 | 19 | ## WireMock JSON Extractor 20 | 21 | wiremock-json-extractor is a WireMock extension that can generate a response from a JSON request. 22 | It recognizes all JSONPaths from the response's template and try to replace them by the correct value 23 | from the request. You can also specify a fallback value that will be use if nothing was found when searching 24 | for the JSONPath. 25 | 26 | ``` 27 | { 28 | "request": { 29 | "method": "POST", 30 | "url": "/some/url" 31 | }, 32 | "response": { 33 | "status": 200, 34 | "body": "I found ${$.value} for $.value. Sadly, I found nothing for ${$.undefined}, 35 | so I will have to use the fallback value: ${$.undefined§3}", 36 | "transformers": ["json-extractor"] 37 | } 38 | } 39 | ``` 40 | 41 | ``` 42 | POST /some/url HTTP/1.1 43 | Content-Type: application/json 44 | { "value": 12 } 45 | ``` 46 | 47 | ``` 48 | HTTP/1.1 200 OK 49 | I found 12 for $.value. Sadly, I found nothing for ${$.undefined}, 50 | so I will have to use the fallback value: 3 51 | ``` 52 | 53 | You can check every supported operators on the [Gatling JSONPath Syntax](https://github.com/gatling/jsonpath#syntax) documentation. 54 | 55 | ## WireMock Calculator 56 | 57 | ``` 58 | { 59 | "request": { 60 | "method": "GET", 61 | "url": "/some/url" 62 | }, 63 | "response": { 64 | "status": 200, 65 | "body": "What is the value of 1+2*3? Simple, it is: ${1+2*3}", 66 | "transformers": ["calculator"] 67 | } 68 | } 69 | ``` 70 | 71 | ``` 72 | GET /some/url HTTP/1.1 73 | ``` 74 | 75 | ``` 76 | HTTP/1.1 200 OK 77 | What is the value of 1+2*3? Simple, it is: 7 78 | ``` 79 | 80 | ## WireMock Randomizer 81 | Random generators supported: 82 | * RandomInteger: integer values from 0 to Integer.MAX_VALUE 83 | * RandomDouble: double values from 0.0 to 1.0 84 | * RandomBoolean: true or false values 85 | * RandomFloat: float values from 0.0 to 1.0 86 | * RandomLong: absolute long values 87 | * RandomString: string of 10 characters 88 | * RandomUUID: random UUID 89 | 90 | ``` 91 | { 92 | "request": { 93 | "method": "GET", 94 | "url": "/some/url" 95 | }, 96 | "response": { 97 | "status": 200, 98 | "body": "Random integer generated: @{RandomInteger}", 99 | "transformers": ["randomizer"] 100 | } 101 | } 102 | ``` 103 | 104 | ``` 105 | GET /some/url HTTP/1.1 106 | ``` 107 | 108 | ``` 109 | HTTP/1.1 200 OK 110 | Random integer generated: 784129741 111 | ``` 112 | 113 | 114 | ## WireMock FreeMarkerRenderer 115 | 116 | wiremock-freemarker-renderer is a WireMock extension that can generate a response from a FreeMarker template. 117 | It maps an incoming JSON request to a FreeMarker data model. $ can be used to call the root object. It allows 118 | a syntax close to JSONPath. 119 | 120 | ``` 121 | { 122 | "request": { 123 | "method": "POST", 124 | "url": "/some/url" 125 | }, 126 | "response": { 127 | "status": 200, 128 | "body": "I found ${$.value} for $.value. Sadly, I found nothing for ${$.undefined!"$.undefined"}, 129 | so I will have to use the fallback value: ${$.undefined!3}", 130 | "transformers": ["freemarker-renderer"] 131 | } 132 | } 133 | ``` 134 | 135 | ``` 136 | POST /some/url HTTP/1.1 137 | Content-Type: application/json 138 | { "value": 12 } 139 | ``` 140 | 141 | ``` 142 | HTTP/1.1 200 OK 143 | I found 12 for $.value. Sadly, I found nothing for $.undefined, 144 | so I will have to use the fallback value: 3 145 | ``` 146 | 147 | You can check all the possibilities on the [FreeMarker](http://freemarker.org/docs/dgui.html) documentation. -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "wiremock-extensions" 2 | organization := "tv.teads" 3 | scalaVersion := "2.11.8" 4 | scalacOptions := Seq("-feature", "-deprecation", "-Xlint") 5 | 6 | libraryDependencies ++= Seq( 7 | "com.github.tomakehurst" % "wiremock-standalone" % "2.8.0" % "provided", 8 | "io.gatling" %% "jsonpath" % "0.6.9", 9 | "com.fasterxml.jackson.core" % "jackson-databind" % "2.9.1", 10 | "net.objecthunter" % "exp4j" % "0.4.8", 11 | "org.freemarker" % "freemarker" % "2.3.26-incubating", 12 | 13 | "org.scalatest" %% "scalatest" % "2.2.6" % "test", 14 | "net.databinder.dispatch" %% "dispatch-core" % "0.12.0" % "test" 15 | ) 16 | 17 | fork in Test := true 18 | 19 | // Release Settings 20 | 21 | def teadsRepo(repo: String) = repo at s"https://nexus.teads.net/content/repositories/$repo" 22 | 23 | publishMavenStyle := true 24 | pomIncludeRepository := { _ => false } 25 | publishTo := Some(if(isSnapshot.value) teadsRepo("snapshots") else teadsRepo("releases")) 26 | 27 | credentials += Credentials(Path.userHome / ".ivy2" / ".credentials") 28 | 29 | // Assembly Settings 30 | 31 | artifact in (Compile, assembly) := { 32 | val art = (artifact in (Compile, assembly)).value 33 | art.copy(`classifier` = Some("assembly")) 34 | } 35 | 36 | addArtifact(artifact in (Compile, assembly), assembly) 37 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.5.1") 2 | 3 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.1") 4 | 5 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.1") 6 | 7 | addMavenResolverPlugin 8 | -------------------------------------------------------------------------------- /scalariform.sbt: -------------------------------------------------------------------------------- 1 | import scalariform.formatter.preferences._ 2 | 3 | scalariformPreferences := FormattingPreferences() 4 | .setPreference(AlignParameters, true) 5 | .setPreference(AlignSingleLineCaseStatements, true) 6 | .setPreference(DoubleIndentClassDeclaration, true) 7 | .setPreference(RewriteArrowSymbols, true) 8 | -------------------------------------------------------------------------------- /src/main/scala/tv/teads/wiremock/extension/Calculator.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock.extension 2 | 3 | import java.text.{DecimalFormat, DecimalFormatSymbols} 4 | 5 | import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder 6 | import com.github.tomakehurst.wiremock.common.FileSource 7 | import com.github.tomakehurst.wiremock.extension.{Parameters, ResponseDefinitionTransformer} 8 | import com.github.tomakehurst.wiremock.http.{Request, ResponseDefinition} 9 | import net.objecthunter.exp4j.ExpressionBuilder 10 | 11 | import scala.annotation.tailrec 12 | import scala.util.Try 13 | import scala.util.matching.Regex 14 | 15 | class Calculator extends ResponseDefinitionTransformer { 16 | 17 | override val getName: String = "calculator" 18 | 19 | override val applyGlobally: Boolean = false 20 | 21 | private val pattern: Regex = """\$\{([ \d\+\-\*\/\(\)\.]+)}""".r 22 | 23 | private val separator: DecimalFormatSymbols = new DecimalFormatSymbols() 24 | separator.setDecimalSeparator('.') 25 | 26 | private val formatter: DecimalFormat = new DecimalFormat("0.#", separator) 27 | 28 | /** 29 | * Transforms a response's body by evaluating mathematical formulas. 30 | * 31 | * @param request a JSON request 32 | * @param responseDefinition the response to transform 33 | */ 34 | override def transform( 35 | request: Request, 36 | responseDefinition: ResponseDefinition, 37 | files: FileSource, 38 | parameters: Parameters 39 | ): ResponseDefinition = { 40 | Try { 41 | ResponseDefinitionBuilder 42 | .like(responseDefinition) 43 | .withBody(replaceCalculus(responseDefinition.getBody)) 44 | .build() 45 | }.getOrElse(responseDefinition) 46 | } 47 | 48 | /** 49 | * Evaluates all formulas in the template which are encapsulated in ${...} 50 | * 51 | * @param template the response to transform 52 | */ 53 | private def replaceCalculus(template: String): String = { 54 | 55 | @tailrec 56 | def rec(template: String, acc: String): String = { 57 | findFirstCalculus(template) match { 58 | case None ⇒ acc + template 59 | case Some(matched) ⇒ 60 | val expression: String = matched.group(1) 61 | val toAdd: String = Try { 62 | val result = BigDecimal { 63 | new ExpressionBuilder(expression) 64 | .build() 65 | .evaluate() 66 | .toString 67 | } 68 | 69 | template.take(matched.start) + formatter.format(result) 70 | }.getOrElse(template.take(matched.end)) 71 | 72 | rec(template.drop(matched.end), acc + toAdd) 73 | } 74 | } 75 | 76 | rec(template, "") 77 | } 78 | 79 | /** 80 | * Finds the first mathematical formula in the template. 81 | */ 82 | private def findFirstCalculus(template: String): Option[Regex.Match] = 83 | pattern.findFirstMatchIn(template) 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/scala/tv/teads/wiremock/extension/FreeMarkerRenderer.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock.extension 2 | 3 | import java.io.{StringReader, StringWriter} 4 | import java.util 5 | 6 | import com.fasterxml.jackson.databind.node.JsonNodeType 7 | import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} 8 | import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder 9 | import com.github.tomakehurst.wiremock.common.FileSource 10 | import com.github.tomakehurst.wiremock.extension.{Parameters, ResponseDefinitionTransformer} 11 | import com.github.tomakehurst.wiremock.http.{Request, ResponseDefinition} 12 | import freemarker.template.{Configuration, _} 13 | 14 | import scala.annotation.tailrec 15 | import scala.collection.JavaConverters._ 16 | import scala.util.{Failure, Success, Try} 17 | 18 | class FreeMarkerRenderer extends ResponseDefinitionTransformer { 19 | 20 | override val getName: String = "freemarker-renderer" 21 | 22 | override val applyGlobally: Boolean = false 23 | 24 | private val mapper: ObjectMapper = new ObjectMapper 25 | private val configuration: Configuration = new Configuration(Configuration.VERSION_2_3_24) 26 | private val wrapper: ObjectWrapper = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_24).build() 27 | 28 | override def transform( 29 | request: Request, 30 | responseDefinition: ResponseDefinition, 31 | files: FileSource, 32 | parameters: Parameters 33 | ): ResponseDefinition = { 34 | Try { 35 | val requestBody: JsonNode = mapper.readTree(request.getBodyAsString) 36 | val template = new Template("template", new StringReader(responseDefinition.getBody), configuration) 37 | 38 | val writer = new StringWriter 39 | template.process(json2hash(wrapper, requestBody), writer) 40 | val body = writer.toString 41 | 42 | ResponseDefinitionBuilder 43 | .like(responseDefinition) 44 | .withBody(body) 45 | .build() 46 | 47 | }.getOrElse(responseDefinition) 48 | } 49 | 50 | private def json2hash(wrapper: ObjectWrapper, node: JsonNode): SimpleHash = { 51 | val hash = new SimpleHash(wrapper) 52 | hash.put("$", json2template(wrapper, node)) 53 | hash.put("findFirstInArray", new FindFirstInArray(node)) 54 | hash 55 | } 56 | 57 | private def json2template(wrapper: ObjectWrapper, node: JsonNode): TemplateModel = node match { 58 | // Values JsonNode 59 | case _ if node.isBigDecimal ⇒ wrapper.wrap(node.decimalValue()) 60 | case _ if node.isBigInteger ⇒ wrapper.wrap(node.bigIntegerValue()) 61 | case _ if node.isBinary ⇒ wrapper.wrap(node.binaryValue()) 62 | case _ if node.isBoolean ⇒ wrapper.wrap(node.booleanValue()) 63 | case _ if node.isDouble ⇒ wrapper.wrap(node.doubleValue()) 64 | case _ if node.isFloat ⇒ wrapper.wrap(node.floatValue()) 65 | case _ if node.isFloatingPointNumber ⇒ wrapper.wrap(node.decimalValue()) 66 | case _ if node.isInt ⇒ wrapper.wrap(node.intValue()) 67 | case _ if node.isIntegralNumber ⇒ wrapper.wrap(node.intValue()) 68 | case _ if node.isLong ⇒ wrapper.wrap(node.longValue()) 69 | case _ if node.isMissingNode ⇒ wrapper.wrap(null) 70 | case _ if node.isNull ⇒ wrapper.wrap(null) 71 | case _ if node.isNumber ⇒ wrapper.wrap(node.numberValue()) 72 | case _ if node.isShort ⇒ wrapper.wrap(node.shortValue()) 73 | case _ if node.isTextual ⇒ wrapper.wrap(node.textValue()) 74 | 75 | // Container JsonNode 76 | case _ if node.isArray ⇒ 77 | val seq = new SimpleSequence(wrapper) 78 | node.elements().asScala.foreach(elem ⇒ seq.add(json2template(wrapper, elem))) 79 | seq 80 | case _ if node.isObject ⇒ 81 | val hash = new SimpleHash(wrapper) 82 | node.fields().asScala.foreach(field ⇒ hash.put(field.getKey, json2template(wrapper, field.getValue))) 83 | hash 84 | } 85 | 86 | class FindFirstInArray(requestBody: JsonNode) extends TemplateMethodModelEx { 87 | import freemarker.template.TemplateModelException 88 | 89 | @tailrec 90 | private def findChildNode(parent: JsonNode, fullPath: String): JsonNode = { 91 | val childNodes = fullPath.split('.') 92 | if (childNodes.length == 1) 93 | parent.findPath(fullPath) 94 | else { 95 | val childPath = childNodes.head 96 | val arrayStartIndex = childPath.indexOf("[") 97 | val parsedChildPath = Try(childPath.substring(0, arrayStartIndex)).toOption.getOrElse(childPath) 98 | val node = parent.findPath(parsedChildPath) 99 | val newParentNode = if (node.isArray) { 100 | (for { 101 | arrayIndex ← Try(childPath.substring(arrayStartIndex + 1, childPath.indexOf("]")).toInt).toOption 102 | element ← Try(node.elements().asScala.toList(arrayIndex)).toOption 103 | } yield element).getOrElse(mapper.createObjectNode().nullNode()) 104 | } else node 105 | findChildNode(newParentNode, childNodes.tail.mkString(".")) 106 | } 107 | } 108 | 109 | private def extractPathAndValueFromCondition(filteredChildCondition: String): (String, String) = { 110 | filteredChildCondition.split("==").map(_.trim).toList match { 111 | case path :: value :: Nil ⇒ (path, value) 112 | case _ ⇒ throw new TemplateModelException("Filtered child condition should be like this : car.color == red") 113 | } 114 | } 115 | 116 | override def exec(arguments: util.List[_]): TemplateModel = { 117 | if (arguments.size() != 3) { 118 | throw new TemplateModelException("Wrong arguments : 3 expected : array node, filtered child condition, wanted node") 119 | } 120 | arguments.asScala.toList.collect { case s: SimpleScalar ⇒ s.getAsString } match { 121 | case List(a1: String, a2: String, a3: String) ⇒ 122 | val (array, filteredChildCondition, wantedChildPath) = (a1, a2, a3) 123 | val arrayNode = requestBody.findPath(array) 124 | 125 | if (!arrayNode.isArray) 126 | throw new TemplateModelException("First arg should be an array node") 127 | 128 | val (filteredChildPath, filteredChildValue) = extractPathAndValueFromCondition(filteredChildCondition) 129 | 130 | arrayNode.elements().asScala 131 | .find(findChildNode(_, filteredChildPath).asText() == filteredChildValue) 132 | .map(findChildNode(_, wantedChildPath)) match { 133 | case Some(wantedNode) ⇒ json2template(wrapper, wantedNode) 134 | case _ ⇒ wrapper.wrap(null) 135 | } 136 | case _ ⇒ throw new TemplateModelException("Invalid arguments types") 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/scala/tv/teads/wiremock/extension/JsonExtractor.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock.extension 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder 5 | import com.github.tomakehurst.wiremock.common.FileSource 6 | import com.github.tomakehurst.wiremock.extension.{Parameters, ResponseDefinitionTransformer} 7 | import com.github.tomakehurst.wiremock.http.{Request, ResponseDefinition} 8 | import io.gatling.jsonpath.FastStringOps.RichString 9 | import io.gatling.jsonpath.JsonPath 10 | 11 | import scala.annotation.tailrec 12 | import scala.util.Try 13 | import scala.util.matching.Regex 14 | 15 | class JsonExtractor extends ResponseDefinitionTransformer { 16 | 17 | case class Matched(all: String, path: String, fallback: Option[String]) 18 | 19 | override val getName: String = "json-extractor" 20 | 21 | override val applyGlobally: Boolean = false 22 | 23 | private val fallbackRegex: Regex = """(?:\§(.+?))?""".r 24 | private val jsonPathRegex: Regex = """(\$\.[ _='a-zA-Z0-9\@\.\[\]\*\,\:\?\(\)\&\|\<\>]*)""".r 25 | private val pattern: Regex = ("""\$\{""" + jsonPathRegex + fallbackRegex + """\}""").r 26 | 27 | private val mapper: ObjectMapper = new ObjectMapper 28 | 29 | /** 30 | * Transforms a response's body by extracting JSONPath and 31 | * replace them from the request. 32 | * 33 | * @param request a JSON request 34 | * @param responseDefinition the response to transform 35 | */ 36 | override def transform( 37 | request: Request, 38 | responseDefinition: ResponseDefinition, 39 | files: FileSource, 40 | parameters: Parameters 41 | ): ResponseDefinition = { 42 | Try { 43 | val requestBody = mapper.readValue(request.getBodyAsString, classOf[Object]) 44 | val template = responseDefinition.getBody 45 | 46 | ResponseDefinitionBuilder 47 | .like(responseDefinition) 48 | .withBody(replacePaths(requestBody, template)) 49 | .build() 50 | }.getOrElse(responseDefinition) 51 | } 52 | 53 | /** 54 | * Replaces all JSONPath in the template which are encapsulated in ${...} 55 | * by searching values in the requestBody. 56 | * 57 | * @param requestBody the JSON used to look for values 58 | * @param template the response to transform 59 | */ 60 | private def replacePaths(requestBody: Any, template: String): String = { 61 | 62 | @tailrec 63 | def rec(requestBody: Any, current: String, previous: String): String = { 64 | if (current.equals(previous)) previous 65 | else { 66 | val nextCurrent: String = 67 | findAllPaths(current).foldLeft(current) { 68 | case (currentAcc, Matched(all, path, fallback)) ⇒ 69 | extractValue(requestBody, path) 70 | .orElse(fallback) 71 | .map(currentAcc.fastReplaceAll(all, _)) 72 | .getOrElse(currentAcc) 73 | } 74 | 75 | rec(requestBody, nextCurrent, current) 76 | } 77 | } 78 | 79 | rec(requestBody, template, "") 80 | } 81 | 82 | /** 83 | * Finds all JSONPaths in the template. 84 | */ 85 | private def findAllPaths(template: String): Set[Matched] = { 86 | pattern.findAllMatchIn(template) 87 | .map(matched ⇒ Matched(matched.matched, matched.group(1), Option(matched.group(2)))) 88 | .toSet 89 | } 90 | 91 | /** 92 | * Extracts the JSONPath value from the requestBody if any 93 | */ 94 | private def extractValue(requestBody: Any, path: String): Option[String] = { 95 | JsonPath 96 | .query(path, requestBody) 97 | .right 98 | .map(_.toList.headOption.map(_.toString)) 99 | .fold(_ ⇒ None, identity) 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/scala/tv/teads/wiremock/extension/Randomizer.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock.extension 2 | 3 | import java.util.UUID 4 | 5 | import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder 6 | import com.github.tomakehurst.wiremock.common.FileSource 7 | import com.github.tomakehurst.wiremock.extension.{Parameters, ResponseDefinitionTransformer} 8 | import com.github.tomakehurst.wiremock.http.{Request, ResponseDefinition} 9 | 10 | import scala.annotation.tailrec 11 | import scala.util.{Random, Try} 12 | import scala.util.matching.Regex 13 | 14 | class Randomizer extends ResponseDefinitionTransformer { 15 | 16 | override def getName: String = "randomizer" 17 | 18 | override val applyGlobally: Boolean = false 19 | 20 | private val randomPattern: Regex = """\@\{([Random\w]+)}""".r 21 | 22 | /** 23 | * Transforms a response's body by generating random values. 24 | * 25 | * @param request a JSON request 26 | * @param responseDefinition the response to transform 27 | */ 28 | override def transform( 29 | request: Request, 30 | responseDefinition: ResponseDefinition, 31 | files: FileSource, 32 | parameters: Parameters 33 | ): ResponseDefinition = Try { 34 | ResponseDefinitionBuilder 35 | .like(responseDefinition) 36 | .withBody(replaceRandom(responseDefinition.getBody)) 37 | .build() 38 | }.getOrElse(responseDefinition) 39 | 40 | /** 41 | * Evaluates all random placeholders in the template which are encapsulated in @{RandomXXX} 42 | * 43 | * @param template the response to transform 44 | */ 45 | private def replaceRandom(template: String): String = { 46 | 47 | @tailrec 48 | def rec(template: String, acc: String): String = { 49 | findFirstRandomPlaceholder(template) match { 50 | case None ⇒ acc + template 51 | case Some(matched) ⇒ 52 | val expression: String = matched.group(1) 53 | val result: String = expression match { 54 | case "RandomInteger" ⇒ String.valueOf(new Random().nextInt(Integer.MAX_VALUE)) 55 | case "RandomDouble" ⇒ String.valueOf(new Random().nextDouble()) 56 | case "RandomBoolean" ⇒ String.valueOf(new Random().nextBoolean()) 57 | case "RandomFloat" ⇒ String.valueOf(new Random().nextFloat()) 58 | case "RandomLong" ⇒ String.valueOf(Math.abs(new Random().nextLong())) 59 | case "RandomString" ⇒ Random.alphanumeric.take(10).mkString 60 | case "RandomUUID" ⇒ UUID.randomUUID().toString 61 | case _ ⇒ acc + template 62 | } 63 | val toAdd: String = template.take(matched.start) + result 64 | 65 | rec(template.drop(matched.end), acc + toAdd) 66 | } 67 | } 68 | 69 | rec(template, acc = "") 70 | } 71 | 72 | /** 73 | * Finds the first random placeholder in the template. 74 | */ 75 | private def findFirstRandomPlaceholder(template: String): Option[Regex.Match] = 76 | randomPattern.findFirstMatchIn(template) 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/test/scala/tv/teads/wiremock/extension/CalculatorSpec.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock.extension 2 | 3 | import java.util.UUID 4 | 5 | import com.ning.http.client.Response 6 | import dispatch.{Future, Http, url} 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | 10 | class CalculatorSpec extends ExtensionSpec { 11 | 12 | val requests: List[(String, String)] = List( 13 | (s"$${1+2}", "3"), 14 | (s"$${1-2}", "-1"), 15 | (s"$${1*2}", "2"), 16 | (s"$${1/2}", "0.5"), 17 | (s"$${1 + 2}", "3"), 18 | (s"$${1.1 + 2.2}", "3.3"), 19 | (s"$${1/0}", s"$${1/0}"), 20 | (s"$${1+2}, $${2+3}", "3, 5") 21 | ) 22 | 23 | "Calculator" should "replace simple calculus in response body" in { 24 | requests.foreach { 25 | case (responseBody, result) ⇒ 26 | val requestUrl = "/" + UUID.randomUUID().toString 27 | 28 | stub("GET", requestUrl, responseBody, "calculator") { 29 | val request: Future[Response] = Http(url(wireMockUrl + requestUrl)) 30 | 31 | validate( 32 | request = request, 33 | result = result, 34 | clue = responseBody, result 35 | ) 36 | } 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/scala/tv/teads/wiremock/extension/CombinationsSpec.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock.extension 2 | 3 | import java.util.UUID 4 | 5 | import com.ning.http.client.Response 6 | import dispatch.{Future, Http, url} 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | 10 | class CombinationsSpec extends ExtensionSpec { 11 | 12 | val requests: List[(String, String, String)] = List( 13 | ("""{"single":"value"}""", s"$${$$.single}", "value"), 14 | ("""{}""", s"$${1+2}", "3"), 15 | ("""{"single":1}""", s"$${$${$$.single} + 2}", "3") 16 | ) 17 | 18 | "JsonExtractor and Calculator" should "combine" in { 19 | requests.foreach { 20 | case (requestBody, responseBody, result) ⇒ 21 | val requestUrl = "/" + UUID.randomUUID().toString 22 | 23 | stub("POST", requestUrl, responseBody, "json-extractor", "calculator") { 24 | val request: Future[Response] = 25 | Http(url(wireMockUrl + requestUrl) 26 | .<<(requestBody) 27 | .setContentType("application/json", "UTF-8")) 28 | 29 | validate( 30 | request = request, 31 | result = result, 32 | clue = requestBody, responseBody, result 33 | ) 34 | } 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/scala/tv/teads/wiremock/extension/ExtensionSpec.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock.extension 2 | 3 | import com.ning.http.client.Response 4 | import dispatch.{Future, Http, Req, url ⇒ httpUrl} 5 | import org.scalatest.concurrent.ScalaFutures 6 | import org.scalatest.time._ 7 | import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} 8 | 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | import scala.util.matching.Regex 11 | 12 | trait ExtensionSpec extends FlatSpec with Matchers with BeforeAndAfterAll with ScalaFutures { 13 | 14 | override implicit val patienceConfig = PatienceConfig(Span(5, Seconds), Span(100, Millis)) 15 | 16 | override def beforeAll(): Unit = { 17 | if (!wireMockServer.isRunning) { 18 | wireMockServer.start() 19 | } 20 | } 21 | 22 | def stub[A]( 23 | method: String, 24 | url: String, 25 | body: String, 26 | transformers: String* 27 | )( 28 | f: ⇒ A 29 | ): A = { 30 | val newMappingBody: String = 31 | s""" 32 | |{ 33 | | "request": { 34 | | "method": "$method", 35 | | "url": "$url" 36 | | }, 37 | | "response": { 38 | | "status": 200, 39 | | "body": "${body.replaceAllLiterally("\"", "\\\"")}", 40 | | "transformers": [${transformers.mkString("\"", "\",\"", "\"")}] 41 | | } 42 | |} 43 | """.stripMargin 44 | 45 | val newMappingRequest: Req = 46 | httpUrl(wireMockUrl + "/__admin/mappings/new") 47 | .POST 48 | .setHeader("Content-Type", "application/json; charset=utf-8") 49 | .setBody(newMappingBody) 50 | 51 | val http: FutureConcept[Response] = Http(newMappingRequest).map { 52 | case response if response.getStatusCode != 201 ⇒ throw new Exception(response.getResponseBody()) 53 | case response ⇒ response 54 | } 55 | 56 | whenReady(http)(_ ⇒ f) 57 | } 58 | 59 | def validate(request: Future[Response], result: String, clue: Any*) = { 60 | whenReady(request) { response ⇒ 61 | withClue(clue.mkString("`", "` | `", "`")) { 62 | response.getResponseBody shouldEqual result 63 | } 64 | } 65 | } 66 | 67 | def validate(request: Future[Response], result: Regex, clue: Any*) = { 68 | whenReady(request) { response ⇒ 69 | withClue(clue.mkString("`", "` | `", "`")) { 70 | response.getResponseBody should fullyMatch regex result 71 | } 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/test/scala/tv/teads/wiremock/extension/FreeMarkerRendererSpec.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock.extension 2 | 3 | import java.util.UUID 4 | 5 | import com.ning.http.client.Response 6 | import dispatch.{Future, Http, url} 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | 10 | class FreeMarkerRendererSpec extends ExtensionSpec { 11 | 12 | val requests: List[(String, String, String, String)] = List( 13 | ("simple case", """{"single":"value"}""", s"$${$$.single}", "value"), 14 | ("fallback", """{}""", s"$${$$.single!1}", "1"), 15 | ("fallback for null nested path", """{}""", s"$${($$.single.value)!1}", "1"), 16 | ("arithmetic", s"""{"value":3}""", s"$${$$.value + 3}", "6"), 17 | ("path as fallback", """{"single":"value"}""", s"$${$$.undefined!$$.single}", "value"), 18 | ("multi fallbacks", "{}", s"""$${$$.undefined!$$.undefined!"value"}""", "value"), 19 | ("array traversal", """{"array":["1","2"]}""", s"""[#ftl][#list $$.array as i]$${i}[/#list]""", """12"""), 20 | ("array glue", """{"array":["1","2"]}""", s"""$${$$.array?join(",")}""", """1,2"""), 21 | ("array find first (simple case)", """{"cheap-cars":[{"details":{"price":15.5, "brand":"toyota"}},{"details":{"price":10,"brand":"lexus"}}]}""", 22 | s"""$${findFirstInArray('cheap-cars', 'details.brand == toyota', 'details.price')?c}""", """15.5"""), 23 | ("array find first (simple case 2)", """{"cheap-cars":[{"details":{"price":15.5, "brand":"toyota"}},{"details":{"price":10,"brand":"lexus"}}]}""", 24 | s"""$${findFirstInArray('cheap-cars', 'details.price == 15.5', 'details.brand')}""", """toyota"""), 25 | ("array find first (missing data)", """{"cheap-cars":[{"details":{"price":15.5, "brand":"toyota"}},{"details":{"price":10,"brand":"lexus"}}]}""", 26 | s"""$${(findFirstInArray('cheap-cars', 'details.brand == unknown', 'details.price')?c)!100}""", """100"""), 27 | ("array find first (array filter in wanted node)", """{"cheap-cars":[{"details":{"price":15.5,"brand":"toyota","customers":[{"name":"Alix","age":44},{"name":"Valentin","age":90}]}},{"details":{"price":10,"brand":"lexus","deals":[{"name":"Tristan","age":32}]}}]}""", 28 | s"""$${(findFirstInArray('cheap-cars', 'details.brand == toyota', 'details.customers[0].name'))!'defaultName'}""", """Alix"""), 29 | ("array find first (missing data / array filter in wanted node)", """{"cheap-cars":[{"details":{"price":15.5,"brand":"toyota","customers":[{"name":"Alix","age":44},{"name":"Valentin","age":90}]}},{"details":{"price":10,"brand":"lexus","deals":[{"name":"Tristan","age":32}]}}]}""", 30 | s"""$${(findFirstInArray('cheap-cars', 'details.brand == unknown', 'details.customers[0].name'))!'defaultName'}""", """defaultName"""), 31 | ("array find first (array filter in wanted node with invalid index)", """{"cheap-cars":[{"details":{"price":15.5,"brand":"toyota","customers":[{"name":"Alix","age":44},{"name":"Valentin","age":90}]}},{"details":{"price":10,"brand":"lexus","deals":[{"name":"Tristan","age":32}]}}]}""", 32 | s"""$${(findFirstInArray('cheap-cars', 'details.brand == toyota', 'details.customers[99].name'))!'defaultName'}""", """defaultName"""), 33 | ("underscore", """{"_single":"value"}""", s"""{"single":"$${$$._single}"}""", """{"single":"value"}""") 34 | ) 35 | 36 | "FreeMarkerRenderer" should "replace template response body" in { 37 | requests.foreach { 38 | case (clue, requestBody, responseBody, result) ⇒ 39 | val requestUrl = "/" + UUID.randomUUID().toString 40 | 41 | stub("POST", requestUrl, responseBody, "freemarker-renderer") { 42 | val request: Future[Response] = 43 | Http(url(wireMockUrl + requestUrl) 44 | .<<(requestBody) 45 | .setContentType("application/json", "UTF-8")) 46 | 47 | validate( 48 | request = request, 49 | result = result, 50 | clue = "case [" + clue + "]" 51 | ) 52 | } 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/test/scala/tv/teads/wiremock/extension/JsonExtractorSpec.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock.extension 2 | 3 | import java.util.UUID 4 | 5 | import com.ning.http.client.Response 6 | import dispatch.{Future, Http, url} 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | 10 | class JsonExtractorSpec extends ExtensionSpec { 11 | 12 | val requests: List[(String, String, String, String)] = List( 13 | ("not found", """{}""", s"$${$$.single}", s"$${$$.single}"), 14 | ("without interpretation", """{"single":"value"}""", s"""$$.single""", s"$$.single"), 15 | ("simple case", """{"single":"value"}""", s"$${$$.single}", "value"), 16 | ("nested value", """{"nested":{"single":"value"}}""", s"$${$$.nested.single}", "value"), 17 | ("array", """{"array":["1","2"]}""", s"$${$$.array[0]}", "1"), 18 | ("not found index", """{"array":["1","2"]}""", s"$${$$.array[2]}", s"$${$$.array[2]}"), 19 | ("multi replacements", """{"single":"value","array":["1","2"]}""", s"$${$$.single} $${$$.array[1]}", "value 2"), 20 | ("found and fallback", """{"single":"value"}""", s"$${$$.single§1}", "value"), 21 | ("not found and fallback", """{}""", s"$${$$.single§1}", "1"), 22 | ("array and fallback", """{"array":["1","2"]}""", s"$${$$.array[2]§3}", "3"), 23 | ("same replacements", """{"single":"value"}""", s"$${$$.single} $${$$.single}", "value value"), 24 | ("mixed found/not found", """{"single":"value"}""", s"$${$$.undefined} $${$$.single}", s"$${$$.undefined} value"), 25 | ("nested replacements", s"""{"single":"value", "path":"$$.single"}""", s"$${$${$$.path}}", "value"), 26 | ("path as fallback", """{"single":"value"}""", s"$${$$.undefined§$${$$.single}}", "value"), 27 | ("multi fallbacks", "{}", s"$${$$.undefined§$${$$.undefined§value}}", "value"), 28 | ("complex template", "{}", s"""{"one":"value", "another":"$${$$.undefined§0}", "last": "one"}""", """{"one":"value", "another":"0", "last": "one"}"""), 29 | ("underscore", """{"test_underscore":"val"}""", s"""{"underscore":"$${$$.test_underscore}"}""", """{"underscore":"val"}""") 30 | ) 31 | 32 | "JsonExtractor" should "replace JSONPath in response body" in { 33 | requests.foreach { 34 | case (clue, requestBody, responseBody, result) ⇒ 35 | val requestUrl = "/" + UUID.randomUUID().toString 36 | 37 | stub("POST", requestUrl, responseBody, "json-extractor") { 38 | val request: Future[Response] = 39 | Http(url(wireMockUrl + requestUrl) 40 | .<<(requestBody) 41 | .setContentType("application/json", "UTF-8")) 42 | 43 | validate( 44 | request = request, 45 | result = result, 46 | clue = "case [" + clue + "]" 47 | ) 48 | } 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/scala/tv/teads/wiremock/extension/RandomizerSpec.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock.extension 2 | 3 | import java.util.UUID 4 | 5 | import com.ning.http.client.Response 6 | import dispatch.{Future, Http, url} 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | import scala.util.matching.Regex 10 | 11 | class RandomizerSpec extends ExtensionSpec { 12 | 13 | val requests: List[(String, Regex)] = List( 14 | ("@{RandomInteger}", """^\d*$""".r), 15 | ("@{RandomLong}", """^\d*$""".r), 16 | ("@{RandomString}", """^\w{10}$""".r), 17 | ("@{RandomBoolean}", """^(true|false)$""".r), 18 | ("@{RandomDouble}", """^(0(\.\d+)?|1(\.0+)?)$""".r), 19 | ("@{RandomFloat}", """^(0(\.\d+)?|1(\.0+)?)$""".r), 20 | ("@{RandomUUID}", """^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$""".r), 21 | ("@{RandomNotFound}", """\@\{RandomNotFound\}""".r) 22 | ) 23 | 24 | "Randomizer" should "replace random placeholders in response body" in { 25 | requests.foreach { 26 | case (responseBody, result) ⇒ 27 | val requestUrl = "/" + UUID.randomUUID().toString 28 | 29 | stub("GET", requestUrl, responseBody, "randomizer") { 30 | val request: Future[Response] = Http(url(wireMockUrl + requestUrl)) 31 | 32 | validate( 33 | request = request, 34 | result = result, 35 | clue = responseBody, result 36 | ) 37 | } 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/scala/tv/teads/wiremock/extension/package.scala: -------------------------------------------------------------------------------- 1 | package tv.teads.wiremock 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer 4 | import com.github.tomakehurst.wiremock.core.WireMockConfiguration._ 5 | 6 | package object extension { 7 | 8 | lazy val wireMockServer: WireMockServer = new WireMockServer( 9 | wireMockConfig() 10 | .port(12345) 11 | .extensions(new JsonExtractor, new Calculator, new FreeMarkerRenderer, new Randomizer) 12 | ) 13 | 14 | lazy val wireMockUrl: String = "http://localhost:" + wireMockServer.port() 15 | 16 | } 17 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.20-SNAPSHOT" --------------------------------------------------------------------------------