diff --git a/src/main/scala/org/iainhull/resttest/Api.scala b/src/main/scala/org/iainhull/resttest/Api.scala index b41d11d..99f14be 100644 --- a/src/main/scala/org/iainhull/resttest/Api.scala +++ b/src/main/scala/org/iainhull/resttest/Api.scala @@ -4,8 +4,8 @@ import java.net.URI import java.net.URLEncoder /** - * Provides the main api for creating and sending REST Web service requests. - * + * Provides the main api for creating and sending REST Web service requests. + * * {{{ * val request = Request(GET, new URI("http://api.rest.org/person", Map(), None)) * val response = driver.execute(request) @@ -15,33 +15,46 @@ import java.net.URLEncoder * None => fail("Expected a body")) * } * }}} - * + * * or using the [[RequestBuilder]] * {{{ * val request = driver.execute(RequestBuilder().withUrl("http://api.rest.org/person/").withMethod(GET)) * }}} - * - * This provides the basic interface used to implement the [[Dsl]], users - * are expected to use the Dsl. + * + * This provides the basic interface used to implement the [[Dsl]], users + * are expected to use the Dsl. */ -object Api { +trait Api { /** The HTTP Methods used to make a request */ - sealed abstract class Method(name: String) - case object GET extends Method("GET") - case object POST extends Method("POST") - case object PUT extends Method("PUT") - case object DELETE extends Method("DELETE") + type Method = Api.Impl.Method + val GET = Api.Impl.GET + val POST = Api.Impl.POST + val PUT = Api.Impl.PUT + val DELETE = Api.Impl.DELETE + val HEAD = Api.Impl.HEAD + val PATCH = Api.Impl.PATCH /** The HTTP Request */ - case class Request(method: Method, url: URI, headers: Map[String, List[String]] = Map(), body: Option[String] = None) - + type Request = Api.Impl.Request + val Request = Api.Impl.Request + /** The HTTP Response */ - case class Response(statusCode: Int, headers: Map[String, List[String]], body: Option[String]) + type Response = Api.Impl.Response + val Response = Api.Impl.Response + + /** The HTTP RequestBuilder */ + type RequestBuilder = Api.Impl.RequestBuilder + val RequestBuilder = Api.Impl.RequestBuilder + + /** + * Abstract interface for submitting REST `Requests` and receiving `Responses` + */ + type HttpClient = Request => Response /** * Convert a sequence of `(name, value)` tuples into a map of headers. * Each tuple creates an entry in the map, duplicate `name`s add the - * `value` to the list. + * `value` to the list. */ def toHeaders(hs: (String, String)*): Map[String, List[String]] = { hs.foldRight(Map[String, List[String]]()) { @@ -65,58 +78,109 @@ object Api { } } - /** - * Abstract interface for submitting REST `Requests` and receiving `Responses` - */ - trait Driver { - def execute(request: Request): Response - } + val Status = Api.Impl.Status +} + +object Api extends Api { /** - * Constants for HTTP Status Codes + * Implementation types used by the [[Api]], these are forward declared in the `Api` trait */ - object Status { - val OK = 200 - val Created = 201 - val Accepted = 202 - val BadRequest = 400 - val Unauthorized = 401 - val PaymentRequired = 402 - val Forbidden = 403 - val NotFound = 404 - } + object Impl { + sealed abstract class Method(name: String) + case object DELETE extends Method("DELETE") + case object GET extends Method("GET") + case object HEAD extends Method("HEAD") + case object PATCH extends Method("PATCH") + case object POST extends Method("POST") + case object PUT extends Method("PUT") - case class RequestBuilder( - method: Option[Method], - url: Option[URI], - query: Seq[(String, String)], - headers: Seq[(String, String)], - queryParams: Seq[(String, String)], - body: Option[String]) { - - def withMethod(method: Method): RequestBuilder = copy(method = Some(method)) - def withUrl(url: String): RequestBuilder = copy(url = Some(new URI(url))) - def withBody(body: String): RequestBuilder = copy(body = Some(body)) - def addPath(path: String): RequestBuilder = { - val s = url.get.toString - val slash = if (s.endsWith("/")) "" else "/" - copy(url = Some(new URI(s + slash + path))) - } - def addHeaders(hs: (String, String)*) = copy(headers = headers ++ hs) - def addQuery(qs: (String, String)*) = copy(queryParams = queryParams ++ qs) + /** The HTTP Request */ + case class Request(method: Method, url: URI, headers: Map[String, List[String]] = Map(), body: Option[String] = None) + + /** The HTTP Response */ + case class Response(statusCode: Int, headers: Map[String, List[String]], body: Option[String]) + + case class RequestBuilder( + method: Option[Method], + url: Option[URI], + query: Seq[(String, String)], + headers: Seq[(String, String)], + queryParams: Seq[(String, String)], + body: Option[String]) { + + def withMethod(method: Method): RequestBuilder = copy(method = Some(method)) + def withUrl(url: String): RequestBuilder = copy(url = Some(new URI(url))) + def withBody(body: String): RequestBuilder = copy(body = Some(body)) + def addPath(path: String): RequestBuilder = { + val s = url.get.toString + val slash = if (s.endsWith("/")) "" else "/" + copy(url = Some(new URI(s + slash + path))) + } + def addHeaders(hs: (String, String)*) = copy(headers = headers ++ hs) + def addQuery(qs: (String, String)*) = copy(queryParams = queryParams ++ qs) + + def toRequest: Request = { + val fullUrl = URI.create(url.get + toQueryString(queryParams: _*)) + Request(method.get, fullUrl, toHeaders(headers: _*), body) + } - def toRequest: Request = { - val fullUrl = URI.create(url.get + toQueryString(queryParams: _*)) - Request(method.get, fullUrl, toHeaders(headers: _*), body) } - } + object RequestBuilder { + implicit val emptyBuilder = RequestBuilder(None, None, Seq(), Seq(), Seq(), None) - object RequestBuilder { - implicit val emptyBuilder = RequestBuilder(None, None, Seq(), Seq(), Seq(), None) + def apply()(implicit builder: RequestBuilder): RequestBuilder = { + builder + } + } - def apply()(implicit builder: RequestBuilder): RequestBuilder = { - builder + /** + * Constants for HTTP Status Codes + */ + object Status { + val Continue = 100 + val SwitchingProtocols = 101 + val OK = 200 + val Created = 201 + val Accepted = 202 + val NonAuthoritativeInformation = 203 + val NoContent = 204 + val ResetContent = 205 + val PartialContent = 206 + val MultipleChoices = 300 + val MovedPermanently = 301 + val Found = 302 + val SeeOther = 303 + val NotModified = 304 + val UseProxy = 305 + val SwitchProxy = 306 + val TemporaryRedirect = 307 + val PermanentRedirect = 308 + val BadRequest = 400 + val Unauthorized = 401 + val PaymentRequired = 402 + val Forbidden = 403 + val NotFound = 404 + val MethodNotAllowed = 405 + val NotAcceptable = 406 + val ProxyAuthenticationRequired = 407 + val RequestTimeout = 408 + val Conflict = 409 + val Gone = 410 + val LengthRequired = 411 + val PreconditionFailed = 412 + val RequestEntityTooLarge = 413 + val RequestUriTooLong = 414 + val UnsupportedMediaType = 415 + val RequestedRangeNotSatisfiable = 416 + val ExpectationFailed = 417 + val InternalServerError = 500 + val NotImplemented = 501 + val BadGateway = 502 + val ServiceUnavailable = 503 + val GatewayTimeout = 504 + val HttpVersionNotSupported = 505 } } } \ No newline at end of file diff --git a/src/main/scala/org/iainhull/resttest/Dsl.scala b/src/main/scala/org/iainhull/resttest/Dsl.scala index 97505b3..c251c8b 100644 --- a/src/main/scala/org/iainhull/resttest/Dsl.scala +++ b/src/main/scala/org/iainhull/resttest/Dsl.scala @@ -90,9 +90,8 @@ import java.net.URI * Exception (failing the test). See [[Extractors]] for more information on the available default `Extractor`s * And how to implement your own. */ -trait Dsl extends Extractors { +trait Dsl extends Api with Extractors { import language.implicitConversions - import Api._ implicit def toRequest(builder: RequestBuilder): Request = builder.toRequest implicit def methodToRequestBuilder(method: Method)(implicit builder: RequestBuilder): RequestBuilder = builder.withMethod(method) @@ -113,15 +112,15 @@ trait Dsl extends Extractors { def /(p: Any) = builder.addPath(p.toString) def :?(params: (Symbol, Any)*) = builder.addQuery(params map (p => (p._1.name, p._2.toString)): _*) - def execute()(implicit driver: Driver): Response = { - driver.execute(builder) + def execute()(implicit client: HttpClient): Response = { + client(builder) } def apply[T](proc: RequestBuilder => T): T = { proc(builder) } - def asserting(assertions: Assertion*)(implicit driver: Driver): Response = { + def asserting(assertions: Assertion*)(implicit client: HttpClient): Response = { val res = execute() val assertionResults = for { a <- assertions @@ -133,18 +132,36 @@ trait Dsl extends Extractors { res } - def expecting[T](func: Response => T)(implicit driver: Driver): T = { + def expecting[T](func: Response => T)(implicit client: HttpClient): T = { val res = execute() func(res) } } + /** + * Extend the default request's configuration so that partially configured requests to be reused. Foe example: + * + * {{{ + * using(_ url "http://api.rest.org/person") { implicit rb => + * GET asserting (StatusCode === Status.OK, jsonBodyAsList[Person] === EmptyList) + * val id = POST body personJson asserting (StatusCode === Status.Created) returning (Header("X-Person-Id")) + * GET / id asserting (StatusCode === Status.OK, jsonBodyAs[Person] === Jason) + * } + * }}} + * + * @param config + * a function to configure the default request + * @param block + * the block of code where the the newly configured request is applied + * @param builder + * the current default request, implicitly resolved, defaults to the empty request + */ def using(config: RequestBuilder => RequestBuilder)(process: RequestBuilder => Unit)(implicit builder: RequestBuilder): Unit = { process(config(builder)) } implicit class RichResponse(response: Response) { - def returning[T1](ext1: ExtractorLike[T1])(implicit driver: Driver): T1 = { + def returning[T1](ext1: ExtractorLike[T1])(implicit client: HttpClient): T1 = { ext1.value(response) } @@ -161,8 +178,8 @@ trait Dsl extends Extractors { } } - implicit def requestBuilderToRichResponse(builder: RequestBuilder)(implicit driver: Driver): RichResponse = new RichResponse(builder.execute()) - implicit def methodToRichResponse(method: Method)(implicit builder: RequestBuilder, driver: Driver): RichResponse = new RichResponse(builder.withMethod(method).execute()) + implicit def requestBuilderToRichResponse(builder: RequestBuilder)(implicit client: HttpClient): RichResponse = new RichResponse(builder.execute()) + implicit def methodToRichResponse(method: Method)(implicit builder: RequestBuilder, client: HttpClient): RichResponse = new RichResponse(builder.withMethod(method).execute()) /** * Add operator support to `Extractor`s these are used to generate an `Assertion` using the extracted value. diff --git a/src/main/scala/org/iainhull/resttest/Extractors.scala b/src/main/scala/org/iainhull/resttest/Extractors.scala index b123aa8..9f1f4cd 100644 --- a/src/main/scala/org/iainhull/resttest/Extractors.scala +++ b/src/main/scala/org/iainhull/resttest/Extractors.scala @@ -4,108 +4,22 @@ import Api._ import scala.util.Try -/** - * Basic trait for all extractors chanes are you want [[Extractor]]. - */ -trait ExtractorLike[+A] { - def name: String - def unapply(res: Response): Option[A] = Try { value(res) }.toOption - def value(implicit res: Response): A -} - -/** - * Primary implementation of ExtractorLike. The name and extraction - * - * @param name - * The name of the extractor - * @param op - * The operation to extract the value of type `A` from the `Response` - */ -case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { - def value(implicit res: Response): A = op(res) - - /** - * Create a new `Extractor` by executing a new function to modify the result. - * Normally followed by `as`. - * - * {{{ - * val JsonBody = BodyText andThen Json.parse as "JsonBody" - * }}} - */ - def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp) - - /** - * Rename the extractor - */ - def as(newName: String) = copy(name = newName) -} - -case class Header(header: String) extends ExtractorLike[String] { - val asText = Extractor[String]("Header(" + header + ")", _.headers(header).mkString(",")) - val asList = Extractor[List[String]]("Header(" + header + ").asList", _.headers(header)) - val asOption = Extractor[Option[List[String]]]("Header(" + header + ").asOption", _.headers.get(header)) - val isDefined = new Object { - def unapply(res: Response): Boolean = { - res.headers.contains(header) - } - } +trait Extractors { + import language.implicitConversions - def ->(value: String): (String, String) = { - (header, value) - } + type ExtractorLike[+A] = Extractors.Impl.ExtractorLike[A] - override def name: String = asText.name - override def unapply(res: Response): Option[String] = asText.unapply(res) - override def value(implicit res: Response): String = asText.value + type Extractor[+A] = Extractors.Impl.Extractor[A] + val Extractor = Extractors.Impl.Extractor -} - -object Header { - val AccessControlAllowOrigin = Header("Access-Control-Allow-Origin") - val AcceptRanges = Header("Accept-Ranges") - val Age = Header("Age") - val Allow = Header("Allow") - val CacheControl = Header("Cache-Control") - val Connection = Header("Connection") - val ContentEncoding = Header("Content-Encoding") - val ContentLanguage = Header("Content-Language") - val ContentLength = Header("Content-Length") - val ContentLocation = Header("Content-Location") - val ContentMd5 = Header("Content-MD5") - val ContentDisposition = Header("Content-Disposition") - val ContentRange = Header("Content-Range") - val ContentType = Header("Content-Type") - val Date = Header("Date") - val ETag = Header("ETag") - val Expires = Header("Expires") - val LastModified = Header("Last-Modified") - val Link = Header("Link") - val Location = Header("Location") - val P3P = Header("P3P") - val Pragma = Header("Pragma") - val ProxyAuthenticate = Header("Proxy-Authenticate") - val Refresh = Header("Refresh") - val RetryAfter = Header("Retry-After") - val Server = Header("Server") - val SetCookie = Header("Set-Cookie") - val StrictTransportSecurity = Header("Strict-Transport-Security") - val Trailer = Header("Trailer") - val TransferEncoding = Header("Transfer-Encoding") - val Vary = Header("Vary") - val Via = Header("Via") - val Warning = Header("Warning") - val WwwAuthenticate = Header("WWW-Authenticate") -} - -trait Extractors { - import language.implicitConversions - import Extractors._ + type Header = Extractors.Impl.Header + val Header = Extractors.Impl.Header val StatusCode = Extractor[Int]("StatusCode", r => r.statusCode) - val Body = Extractor[Option[String]]("bodyOption", r => r.body) + val Body = Extractor[Option[String]]("Body", r => r.body) - val BodyText = Extractor[String]("body", r => r.body.get) + val BodyText = Extractor[String]("BodyText", r => r.body.get) /** * Enable Extractors to be chained together in case clauses. @@ -127,4 +41,104 @@ trait Extractors { } object Extractors extends Extractors { + object Impl { + /** + * Basic trait for all extractors chanes are you want [[Extractor]]. + */ + trait ExtractorLike[+A] { + def name: String + def unapply(res: Response): Option[A] = Try { value(res) }.toOption + def value(implicit res: Response): A + } + + /** + * Primary implementation of ExtractorLike. The name and extraction + * + * @param name + * The name of the extractor + * @param op + * The operation to extract the value of type `A` from the `Response` + */ + case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] { + def value(implicit res: Response): A = op(res) + + /** + * Create a new `Extractor` by executing a new function to modify the result. + * Normally followed by `as`. + * + * {{{ + * val JsonBody = BodyText andThen Json.parse as "JsonBody" + * }}} + */ + def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp) + + /** + * Rename the extractor + */ + def as(newName: String) = copy(name = newName) + } + + /** + * Defines `Extractor`s for the specified header, specific extractors provided by the `asText`, `asList`, `asOption` members. + * Instances behave like their `asText` member. + */ + case class Header(header: String) extends ExtractorLike[String] { + val asText = Extractor[String]("Header(" + header + ")", _.headers(header).mkString(",")) + val asList = Extractor[List[String]]("Header(" + header + ").asList", _.headers(header)) + val asOption = Extractor[Option[List[String]]]("Header(" + header + ").asOption", _.headers.get(header)) + val isDefined = new Object { + def unapply(res: Response): Boolean = { + res.headers.contains(header) + } + } + + def ->(value: String): (String, String) = { + (header, value) + } + + override def name: String = asText.name + override def unapply(res: Response): Option[String] = asText.unapply(res) + override def value(implicit res: Response): String = asText.value + } + + /** + * Provides constants for standard headers. + */ + object Header { + val AccessControlAllowOrigin = Header("Access-Control-Allow-Origin") + val AcceptRanges = Header("Accept-Ranges") + val Age = Header("Age") + val Allow = Header("Allow") + val CacheControl = Header("Cache-Control") + val Connection = Header("Connection") + val ContentEncoding = Header("Content-Encoding") + val ContentLanguage = Header("Content-Language") + val ContentLength = Header("Content-Length") + val ContentLocation = Header("Content-Location") + val ContentMd5 = Header("Content-MD5") + val ContentDisposition = Header("Content-Disposition") + val ContentRange = Header("Content-Range") + val ContentType = Header("Content-Type") + val Date = Header("Date") + val ETag = Header("ETag") + val Expires = Header("Expires") + val LastModified = Header("Last-Modified") + val Link = Header("Link") + val Location = Header("Location") + val P3P = Header("P3P") + val Pragma = Header("Pragma") + val ProxyAuthenticate = Header("Proxy-Authenticate") + val Refresh = Header("Refresh") + val RetryAfter = Header("Retry-After") + val Server = Header("Server") + val SetCookie = Header("Set-Cookie") + val StrictTransportSecurity = Header("Strict-Transport-Security") + val Trailer = Header("Trailer") + val TransferEncoding = Header("Transfer-Encoding") + val Vary = Header("Vary") + val Via = Header("Via") + val Warning = Header("Warning") + val WwwAuthenticate = Header("WWW-Authenticate") + } + } } \ No newline at end of file diff --git a/src/main/scala/org/iainhull/resttest/JerseyDriver.scala b/src/main/scala/org/iainhull/resttest/JerseyDriver.scala index 2213b5d..a0ce1ed 100644 --- a/src/main/scala/org/iainhull/resttest/JerseyDriver.scala +++ b/src/main/scala/org/iainhull/resttest/JerseyDriver.scala @@ -7,43 +7,46 @@ import com.sun.jersey.api.client.WebResource object Jersey { import Api._ - - implicit object Driver extends Driver { - val jersey = Client.create() - def execute(request: Request): Response = { - val response = createClientResponse(request) - Response(response.getStatus, headers(response), Some(response.getEntity(classOf[String]))) - } + implicit val HttpClient: HttpClient = { request => + val response = Impl.createClientResponse(request) + Response(response.getStatus, Impl.headers(response), Some(response.getEntity(classOf[String]))) + } + + object Impl { + + val jersey = Client.create() def createClientResponse(request: Request): ClientResponse = { val builder = addRequestHeaders(request.headers, jersey.resource(request.url).getRequestBuilder) - + for (b <- request.body) { - builder.entity(b) + builder.entity(b) } - + request.method match { case GET => builder.get(classOf[ClientResponse]) case POST => builder.post(classOf[ClientResponse]) case PUT => builder.put(classOf[ClientResponse]) case DELETE => builder.delete(classOf[ClientResponse]) + case HEAD => builder.method("HEAD", classOf[ClientResponse]) + case PATCH => builder.method("PATCH", classOf[ClientResponse]) } } - + def addRequestHeaders(headers: Map[String, List[String]], builder: WebResource#Builder): WebResource#Builder = { def forAllNames(names: List[String], b: WebResource#Builder): WebResource#Builder = { names match { case h :: t => forAllNames(t, forAllValues(h, headers(h), b)) case Nil => b } - } + } def forAllValues(name: String, values: List[String], b: WebResource#Builder): WebResource#Builder = { values match { case h :: t => forAllValues(name, t, b.header(name, h)) case Nil => b } - } + } forAllNames(headers.keys.toList, builder) } diff --git a/src/main/scala/org/iainhull/resttest/JsonExtractors.scala b/src/main/scala/org/iainhull/resttest/JsonExtractors.scala index 24cd8d8..ba8fb86 100644 --- a/src/main/scala/org/iainhull/resttest/JsonExtractors.scala +++ b/src/main/scala/org/iainhull/resttest/JsonExtractors.scala @@ -6,9 +6,8 @@ import play.api.libs.json.Reads import play.api.libs.json.JsArray import play.api.libs.json.Json -trait JsonExtractors extends Extractors { - import Api._ - import Dsl._ +trait JsonExtractors { + import Extractors._ /** * Extract a path from a json document and deserialise it to a List @@ -61,4 +60,4 @@ trait JsonExtractors extends Extractors { } } -object JsonExtractors extends JsonExtractors with Dsl +object JsonExtractors extends JsonExtractors diff --git a/src/main/scala/org/iainhull/resttest/RestMatchers.scala b/src/main/scala/org/iainhull/resttest/RestMatchers.scala index 29a8a8b..fc1781c 100644 --- a/src/main/scala/org/iainhull/resttest/RestMatchers.scala +++ b/src/main/scala/org/iainhull/resttest/RestMatchers.scala @@ -9,10 +9,10 @@ import org.scalatest.Assertions /** * Adds [[http://www.scalatest.org/ ScalaTest]] support to the RestTest [[Dsl]]. * - * The `should` keyword is added to [[RequestBuilder]] and [[Response]] expressions, the + * The `should` keyword is added to [[Api.RequestBuilder]] and [[Api.Response]] expressions, the * `RequestBuilder` is executed first and `should` applied to the `Response`. * - * The `have` keyword supports [[ExtractorLike]]s. See [[ExtractorToHavePropertyMatcher]] for more details. + * The `have` keyword supports [[Extractors.ExtractorLike]]s. See [[ExtractorToHavePropertyMatcher]] for more details. * * == Example == * @@ -41,16 +41,16 @@ trait RestMatchers { import Dsl._ /** - * Implicitly execute a [[RequestBuilder]] and convert the [[Response]] into a `AnyRefShouldWrapper` + * Implicitly execute a [[Api.RequestBuilder]] and convert the [[Api.Response]] into a `AnyRefShouldWrapper` * * This adds support for ScalaTest's `ShouldMatchers` to `RequestBuilder` */ - implicit def requestBuilderToShouldWrapper(builder: RequestBuilder)(implicit driver: Driver): AnyRefShouldWrapper[Response] = { + implicit def requestBuilderToShouldWrapper(builder: RequestBuilder)(implicit client: HttpClient): AnyRefShouldWrapper[Response] = { responseToShouldWrapper(builder execute ()) } /** - * Implicitly convert a [[Response]] into a `AnyRefShouldWrapper` + * Implicitly convert a [[Api.Response]] into a `AnyRefShouldWrapper` * * This adds support for ScalaTest's `ShouldMatchers` to `Response` */ @@ -58,7 +58,7 @@ trait RestMatchers { new AnyRefShouldWrapper(response) } - implicit def methodToShouldWrapper(method: Method)(implicit builder: RequestBuilder, driver: Driver): AnyRefShouldWrapper[Response] = { + implicit def methodToShouldWrapper(method: Method)(implicit builder: RequestBuilder, client: HttpClient): AnyRefShouldWrapper[Response] = { requestBuilderToShouldWrapper(builder.withMethod(method)) } @@ -70,7 +70,7 @@ trait RestMatchers { } /** - * Implicitly add operations to [[Extractor]] that create `HavePropertyMatcher`s. + * Implicitly add operations to [[Extractors.Extractor]] that create `HavePropertyMatcher`s. * * This adds support for reusing `Extractor`s in `should have(...)` expressions, for example * @@ -94,7 +94,7 @@ trait RestMatchers { } /** - * Implicitly convert an [[Extractor]] that returns any type of `Option` into a `HavePropertyMatcher`. + * Implicitly convert an [[Extractors.Extractor]] that returns any type of `Option` into a `HavePropertyMatcher`. * * This adds support for reusing `Extractor[Option[_]]`s in `should have(...)` expressions, for example * diff --git a/src/test/scala/org/iainhull/resttest/ApiSpec.scala b/src/test/scala/org/iainhull/resttest/ApiSpec.scala index 3dc4c4b..42ade2a 100644 --- a/src/test/scala/org/iainhull/resttest/ApiSpec.scala +++ b/src/test/scala/org/iainhull/resttest/ApiSpec.scala @@ -10,27 +10,22 @@ import org.scalatest.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) class ApiSpec extends FlatSpec with ShouldMatchers { import Api._ - - val driver = new Driver { - def execute(request: Request): Response = { - Response(200, Map("X-Person-Id" -> List("1234")), None) - } - } + import TestData._ "A Simple Driver" should "take a request and return a static response" in { - val response = driver.execute(Request(method = GET, url = new URI("http://api.rest.org/person"))) + val response = TestClient(Request(method = GET, url = new URI("http://api.rest.org/person"))) response.statusCode should be(Status.OK) } "The Api" should "support a simple rest use case, if a little long winded" in { val personJson = """{ "name": "Jason" }""" - val r1 = driver.execute(Request(GET, new URI("http://api.rest.org/person/"), Map(), None)) - val r2 = driver.execute(Request(POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) + val r1 = TestClient(Request(GET, new URI("http://api.rest.org/person/"), Map(), None)) + val r2 = TestClient(Request(POST, new URI("http://api.rest.org/person/"), Map(), Some(personJson))) val id = r2.headers("X-Person-Id").head - val r3 = driver.execute(Request(GET, new URI("http://api.rest.org/person/" + id), Map(), None)) - val r4 = driver.execute(Request(GET, new URI("http://api.rest.org/person/"), Map(), None)) - val r5 = driver.execute(Request(DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) - val r6 = driver.execute(Request(GET, new URI("http://api.rest.org/person/"), Map(), None)) + val r3 = TestClient(Request(GET, new URI("http://api.rest.org/person/" + id), Map(), None)) + val r4 = TestClient(Request(GET, new URI("http://api.rest.org/person/"), Map(), None)) + val r5 = TestClient(Request(DELETE, new URI("http://api.rest.org/person/" + id), Map(), None)) + val r6 = TestClient(Request(GET, new URI("http://api.rest.org/person/"), Map(), None)) } "A RequestBuilder" should "simplify the creation of request objects" in { diff --git a/src/test/scala/org/iainhull/resttest/DslSpec.scala b/src/test/scala/org/iainhull/resttest/DslSpec.scala index 705539c..6f2def8 100644 --- a/src/test/scala/org/iainhull/resttest/DslSpec.scala +++ b/src/test/scala/org/iainhull/resttest/DslSpec.scala @@ -12,11 +12,10 @@ import play.api.libs.json._ @RunWith(classOf[JUnitRunner]) class DslSpec extends FlatSpec with ShouldMatchers { - import Api._ import Dsl._ import TestData._ - implicit val driver = newTestDriver + implicit val driver = TestClient "The DSL" should "support a basic rest use case with a RequestBuilder" in { RequestBuilder().withMethod(GET).withUrl("http://api.rest.org/person/").toRequest should @@ -189,34 +188,34 @@ class DslSpec extends FlatSpec with ShouldMatchers { * to a test above to verify the functionality. */ "Sample use-case" should "support a basic rest use case with a RequestBuilder" in { - val r1 = driver.execute(RequestBuilder().withMethod(GET).withUrl("http://api.rest.org/person/")) - val r2 = driver.execute(RequestBuilder().withMethod(POST).withUrl("http://api.rest.org/person/").withBody(personJson)) + val r1 = TestClient(RequestBuilder().withMethod(GET).withUrl("http://api.rest.org/person/")) + val r2 = TestClient(RequestBuilder().withMethod(POST).withUrl("http://api.rest.org/person/").withBody(personJson)) val id = r2.headers.get("X-Person-Id").get.head - val r3 = driver.execute(RequestBuilder().withMethod(GET).withUrl("http://api.rest.org/person/").addPath(id)) - val r4 = driver.execute(RequestBuilder().withMethod(GET).withUrl("http://api.rest.org/person/")) - val r5 = driver.execute(RequestBuilder().withMethod(DELETE).withUrl("http://api.rest.org/person/").addPath(id)) - val r6 = driver.execute(RequestBuilder().withMethod(GET).withUrl("http://api.rest.org/person/")) + val r3 = TestClient(RequestBuilder().withMethod(GET).withUrl("http://api.rest.org/person/").addPath(id)) + val r4 = TestClient(RequestBuilder().withMethod(GET).withUrl("http://api.rest.org/person/")) + val r5 = TestClient(RequestBuilder().withMethod(DELETE).withUrl("http://api.rest.org/person/").addPath(id)) + val r6 = TestClient(RequestBuilder().withMethod(GET).withUrl("http://api.rest.org/person/")) } it should "support a basic rest use case, reusing a RequestBuilder" in { val rb = RequestBuilder().withUrl("http://api.rest.org/person/") - val r1 = driver.execute(rb.withMethod(GET)) - val r2 = driver.execute(rb.withMethod(POST).withBody(personJson)) + val r1 = TestClient(rb.withMethod(GET)) + val r2 = TestClient(rb.withMethod(POST).withBody(personJson)) val id = r2.headers.get("X-Person-Id").get.head - val r3 = driver.execute(rb.withMethod(GET).addPath(id)) - val r4 = driver.execute(rb.withMethod(GET)) - val r5 = driver.execute(rb.withMethod(DELETE).addPath(id)) - val r6 = driver.execute(rb.withMethod(GET)) + val r3 = TestClient(rb.withMethod(GET).addPath(id)) + val r4 = TestClient(rb.withMethod(GET)) + val r5 = TestClient(rb.withMethod(DELETE).addPath(id)) + val r6 = TestClient(rb.withMethod(GET)) } it should "support a basic rest use case, with Method boostrapping the DSL and infix notation" in { - val r1 = driver.execute(GET withUrl "http://api.rest.org/person/") - val r2 = driver.execute(POST withUrl "http://api.rest.org/person/" withBody personJson) + val r1 = TestClient(GET withUrl "http://api.rest.org/person/") + val r2 = TestClient(POST withUrl "http://api.rest.org/person/" withBody personJson) val id = r2.headers.get("X-Person-Id").get.head - val r3 = driver.execute(GET withUrl "http://api.rest.org/person/" addPath id) - val r4 = driver.execute(GET withUrl "http://api.rest.org/person/") - val r5 = driver.execute(DELETE withUrl "http://api.rest.org/person/" addPath id) - val r6 = driver.execute(GET withUrl "http://api.rest.org/person/") + val r3 = TestClient(GET withUrl "http://api.rest.org/person/" addPath id) + val r4 = TestClient(GET withUrl "http://api.rest.org/person/") + val r5 = TestClient(DELETE withUrl "http://api.rest.org/person/" addPath id) + val r6 = TestClient(GET withUrl "http://api.rest.org/person/") } it should "support a basic rest use case, with Method boostrapping the DSL and execute method" in { @@ -323,7 +322,7 @@ class DslSpec extends FlatSpec with ShouldMatchers { case StatusCode(Status.OK) & BodyAsPersonList(EmptyList) => } val id = POST body personJson expecting { - case StatusCode(Status.Created) ~ PersonIdHeader(id) => id + case StatusCode(Status.Created) & PersonIdHeader(id) => id } GET / id expecting { case StatusCode(Status.OK) & BodyAsPerson(p) => p should be(Jason) diff --git a/src/test/scala/org/iainhull/resttest/ExtractorsSpec.scala b/src/test/scala/org/iainhull/resttest/ExtractorsSpec.scala index 9abe5e6..ba738ff 100644 --- a/src/test/scala/org/iainhull/resttest/ExtractorsSpec.scala +++ b/src/test/scala/org/iainhull/resttest/ExtractorsSpec.scala @@ -7,7 +7,6 @@ import org.scalatest.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) class ExtractorsSpec extends FlatSpec with ShouldMatchers { - import Api._ import Dsl._ val response = Response(Status.OK, toHeaders("SimpleHeader" -> "SimpleValue", "MultiHeader" -> "Value1", "MultiHeader" -> "Value2"), Some("body")) diff --git a/src/test/scala/org/iainhull/resttest/JsonExtractorsSpec.scala b/src/test/scala/org/iainhull/resttest/JsonExtractorsSpec.scala index f36c18e..89d71d6 100644 --- a/src/test/scala/org/iainhull/resttest/JsonExtractorsSpec.scala +++ b/src/test/scala/org/iainhull/resttest/JsonExtractorsSpec.scala @@ -9,7 +9,6 @@ import play.api.libs.functional.syntax._ @RunWith(classOf[JUnitRunner]) class JsonExtractorsSpec extends FlatSpec with ShouldMatchers { - import Api._ import Dsl._ import JsonExtractors._ import TestData._ diff --git a/src/test/scala/org/iainhull/resttest/RestMatchersSpec.scala b/src/test/scala/org/iainhull/resttest/RestMatchersSpec.scala index d00cb72..07204a0 100644 --- a/src/test/scala/org/iainhull/resttest/RestMatchersSpec.scala +++ b/src/test/scala/org/iainhull/resttest/RestMatchersSpec.scala @@ -12,11 +12,10 @@ class RestMatcherSpec extends FlatSpec with ShouldMatchers { import language.implicitConversions import TestData._ - import Api._ import Dsl._ import RestMatchers._ - implicit val driver = newTestDriver + implicit val driver = TestClient val response = Response(Status.OK, toHeaders("header1" -> "", "header2" -> "value", "header3" -> "value1", "header3" -> "value2"), None) diff --git a/src/test/scala/org/iainhull/resttest/TestData.scala b/src/test/scala/org/iainhull/resttest/TestData.scala index 5326952..a7d268c 100644 --- a/src/test/scala/org/iainhull/resttest/TestData.scala +++ b/src/test/scala/org/iainhull/resttest/TestData.scala @@ -65,7 +65,7 @@ object TestData { (__ \ "email").read[String])(Person) - class TestDriver extends Driver { + object TestClient extends HttpClient { val defaultResponse = Response(200, Map("X-Person-Id" -> List("1234")), Some("body")) var responses = List[Response]() var requests = List[Request]() @@ -74,7 +74,7 @@ object TestData { def nextResponse = responses.headOption.getOrElse(defaultResponse) def nextResponse_=(response: Response) = responses = List(response) - def execute(request: Request): Response = { + override def apply(request: Request): Response = { requests = request :: requests if (!responses.isEmpty) { val response = responses.head @@ -85,6 +85,4 @@ object TestData { } } } - - def newTestDriver = new TestDriver } \ No newline at end of file