Skip to content

Commit fac0e74

Browse files
committed
Added generic LambdaHandlerSpec
1 parent d72768a commit fac0e74

File tree

4 files changed

+174
-66
lines changed

4 files changed

+174
-66
lines changed

build.sbt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ lazy val root = project
77
.in(file("."))
88
.settings(commonSettings)
99
.settings(noPublishSettings)
10-
.aggregate(common, http4s, exampleHttp4s)
10+
.aggregate(common, tests, http4s, exampleHttp4s)
1111

1212
lazy val CirceVersion = "0.9.0"
1313
lazy val ScalaTestVersion = "3.0.4"
@@ -26,6 +26,19 @@ lazy val common = project
2626
)
2727
)
2828

29+
lazy val tests = project
30+
.in(file("tests"))
31+
.settings(commonSettings)
32+
.settings(noPublishSettings)
33+
.settings(
34+
moduleName := "tests",
35+
libraryDependencies ++=
36+
Seq(
37+
"org.scalatest" %% "scalatest" % ScalaTestVersion
38+
)
39+
)
40+
.dependsOn(common)
41+
2942
lazy val http4s = project
3043
.in(file("http4s-lambda"))
3144
.settings(publishSettings)
@@ -48,6 +61,7 @@ lazy val http4s = project
4861
}
4962
)
5063
.dependsOn(common)
64+
.dependsOn(tests % "test")
5165

5266
lazy val exampleHttp4s = project
5367
.in(file("example-http4s"))

http4s-lambda/src/main/scala/io/github/howardjohn/lambda/http4s/Http4sLambdaHandler.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.github.howardjohn.lambda.http4s
22

3-
import cats.effect.{Effect, IO}
3+
import cats.effect.IO
44
import fs2.{text, Stream}
55
import io.github.howardjohn.lambda.ProxyEncoding._
66
import io.github.howardjohn.lambda.{ProxyEncoding, LambdaHandler}
Lines changed: 23 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,29 @@
11
package io.github.howardjohn.lambda.http4s
22

3-
import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream}
4-
import java.nio.charset.StandardCharsets
5-
63
import cats.effect.IO
7-
import io.circe.generic.auto._
8-
import io.circe.parser.decode
9-
import io.circe.syntax._
10-
import io.github.howardjohn.lambda.ProxyEncoding.{ProxyRequest, ProxyResponse}
11-
import org.http4s._
12-
import org.http4s.circe._
4+
import io.github.howardjohn.lambda.LambdaHandlerBehavior
5+
import io.github.howardjohn.lambda.LambdaHandlerBehavior._
6+
import org.http4s.{Header, HttpService}
137
import org.http4s.dsl.io._
14-
import org.scalatest.{FlatSpec, Matchers}
15-
16-
class Http4sLambdaHandlerSpec extends FlatSpec with Matchers {
17-
import Http4sLambdaHandlerSpec._
18-
19-
"handle" should "return the body with needed headers" in {
20-
val service: HttpService[IO] = HttpService[IO] {
21-
case _ => Ok("response")
22-
}
23-
24-
val response = doHandle(new Http4sLambdaHandler(service), ProxyRequest("GET", "/", None, None, None))
25-
assert(response.body == "response")
26-
assert(response.statusCode == 200)
27-
}
28-
29-
it should "handle POST body" in {
30-
case class Input(
31-
data: Seq[String],
32-
)
33-
implicit val inputDecoder = jsonOf[IO, Input]
34-
val service: HttpService[IO] = HttpService[IO] {
35-
case req @ POST -> Root =>
36-
for {
37-
inp <- req.as[Input]
38-
resp <- Ok(inp.data.head)
39-
} yield resp
40-
}
41-
42-
val response =
43-
doHandle(new Http4sLambdaHandler(service), ProxyRequest("POST", "/", None, Some("""{"data":["a","b"]}"""), None))
44-
assert(response.body == "a")
45-
}
46-
47-
it should "return not found on a route miss" in {
48-
val service: HttpService[IO] = HttpService[IO] {
49-
case GET -> Root / "api" => Ok("Success")
50-
}
51-
52-
val response = doHandle(new Http4sLambdaHandler(service), ProxyRequest("GET", "/", None, None, None))
53-
assert(response.statusCode == 404)
54-
}
55-
}
56-
57-
object Http4sLambdaHandlerSpec {
58-
59-
def toStream(source: String): InputStream =
60-
new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8))
61-
62-
def doHandle(handler: Http4sLambdaHandler, input: ProxyRequest): ProxyResponse =
63-
doHandle(handler, input.asJson.noSpaces)
64-
65-
def doHandle(handler: Http4sLambdaHandler, input: String): ProxyResponse = {
66-
val os = new ByteArrayOutputStream
67-
handler.handle(toStream(input), os)
68-
decode[ProxyResponse](new String(os.toByteArray, "UTF-8")).right.get
8+
import org.scalatest.{FeatureSpec, GivenWhenThen}
9+
10+
class Http4sLambdaHandlerSpec extends FeatureSpec with LambdaHandlerBehavior with GivenWhenThen {
11+
object TimesQueryMatcher extends OptionalQueryParamDecoderMatcher[Int]("times")
12+
val route = HttpService[IO] {
13+
case GET -> Root / "hello" :? TimesQueryMatcher(times) =>
14+
Ok {
15+
Seq
16+
.fill(times.getOrElse(1))("Hello World!")
17+
.mkString(" ")
18+
}
19+
case GET -> Root / "long" => IO(Thread.sleep(1000)).flatMap(_ => Ok("Hello World!"))
20+
case GET -> Root / "exception" => throw RouteException()
21+
case GET -> Root / "error" => InternalServerError()
22+
case req @ GET -> Root / "header" =>
23+
val header = req.headers.find(h => h.name.value == inputHeader).map(_.value).getOrElse("Header Not Found")
24+
Ok(header, Header(outputHeader, outputHeaderValue))
25+
case req @ POST -> Root / "post" => req.as[String].flatMap(s => Ok(s))
6926
}
27+
val handler = new Http4sLambdaHandler(route)
28+
scenariosFor(behavior(handler))
7029
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package io.github.howardjohn.lambda
2+
3+
import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
4+
5+
import io.circe.generic.auto._
6+
import io.circe.parser.decode
7+
import io.circe.syntax._
8+
import io.github.howardjohn.lambda.ProxyEncoding.{ProxyRequest, ProxyResponse}
9+
import org.scalatest.Assertions._
10+
import org.scalatest.{FeatureSpec, GivenWhenThen}
11+
12+
/**
13+
* Defines the behavior a LambdaHandler must implement. They should be tested against a set of the following routes:
14+
* GET /hello => "Hello World!"
15+
* GET /hello?times=3 => "Hello World! Hello World! Hello World!"
16+
* GET /long => takes a second to complete
17+
* POST /post with body => responds with the body
18+
* GET /exception => throws a RouteException
19+
* GET /error => responds with a 500 error code
20+
* GET /header with header InputHeader => responds with a new header,
21+
* OutputHeader: outputHeaderValue and the value of InputHeader as the body
22+
*/
23+
trait LambdaHandlerBehavior { this: FeatureSpec with GivenWhenThen =>
24+
import LambdaHandlerBehavior._
25+
26+
def behavior(handler: LambdaHandler) {
27+
scenario("A simple get request is made") {
28+
Given("a GET request to /hello")
29+
val response = runRequest("/hello")(handler)
30+
31+
Then("the status code should be 200")
32+
assert(response.statusCode === 200)
33+
34+
And("the body should be Hello World!")
35+
assert(response.body === "Hello World!")
36+
}
37+
38+
scenario("A bad http method is used on a route") {
39+
Given("a POST request to /hello")
40+
val response = runRequest("/hello", httpMethod = "POST")(handler)
41+
42+
Then("the status code should be 404")
43+
assert(response.statusCode === 404)
44+
}
45+
46+
scenario("Including query parameters in a call") {
47+
Given("a GET request to /hello?times=3")
48+
val response = runRequest("/hello", query = Some(Map("times" -> "3")))(handler)
49+
50+
Then("the status code should be 200")
51+
assert(response.statusCode === 200)
52+
53+
And("the body should be Hello World! Hello World! Hello World!")
54+
assert(response.body === "Hello World! Hello World! Hello World!")
55+
}
56+
57+
scenario("A request takes a long time to respond") {
58+
Given("a GET request to /long")
59+
val response = runRequest("/long")(handler)
60+
61+
Then("the status code should be 200")
62+
assert(response.statusCode === 200)
63+
}
64+
65+
scenario("A POST call is made with a body") {
66+
Given("a POST request to /post")
67+
val response = runRequest("/post", body = Some("body"), httpMethod = "POST")(handler)
68+
69+
Then("the status code should be 200")
70+
assert(response.statusCode === 200)
71+
72+
And("the body should be body")
73+
assert(response.body === "body")
74+
}
75+
76+
scenario("A request causes an exception") {
77+
Given("a GET request to /exception")
78+
Then("an exception should be thrown")
79+
intercept[RouteException](runRequest("/exception")(handler))
80+
}
81+
82+
scenario("A request returns an error response") {
83+
Given("a GET request to /error")
84+
val response = runRequest("/error")(handler)
85+
86+
Then("the status code should be 500")
87+
assert(response.statusCode === 500)
88+
}
89+
90+
scenario("Request and responding with headers") {
91+
Given("a GET request to /header")
92+
val response = runRequest("/header", headers = Some(Map(inputHeader -> inputHeaderValue)))(handler)
93+
94+
Then("the status code should be 200")
95+
assert(response.statusCode === 200)
96+
97+
And("the body should be inputHeaderValue")
98+
assert(response.body === inputHeaderValue)
99+
100+
And("the headers should include outputHeader")
101+
assert(response.headers.get(outputHeader) === Some(outputHeaderValue))
102+
}
103+
}
104+
}
105+
106+
object LambdaHandlerBehavior {
107+
case class RouteException() extends RuntimeException("There was an exception in the route")
108+
val outputHeader = "OutputHeader"
109+
val outputHeaderValue = "outputHeaderValue"
110+
val inputHeader = "InputHeader"
111+
val inputHeaderValue = "inputHeaderValue"
112+
113+
private def runRequest(
114+
path: String,
115+
httpMethod: String = "GET",
116+
headers: Option[Map[String, String]] = None,
117+
body: Option[String] = None,
118+
query: Option[Map[String, String]] = None
119+
)(handler: LambdaHandler): ProxyResponse = {
120+
val request = ProxyRequest(
121+
httpMethod,
122+
path,
123+
headers,
124+
body,
125+
query
126+
).asJson.noSpaces
127+
val input = new ByteArrayInputStream(request.getBytes("UTF-8"))
128+
val output = new ByteArrayOutputStream()
129+
handler.handle(input, output)
130+
decode[ProxyResponse](new String(output.toByteArray)) match {
131+
case Left(err) => fail(err)
132+
case Right(resp) => resp
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)