Skip to content

Commit

Permalink
Merge pull request #560 from disneystreaming/main
Browse files Browse the repository at this point in the history
main into series/0.17
  • Loading branch information
Baccata authored Oct 28, 2022
2 parents b650e39 + 4e1c24a commit 5bac4e9
Show file tree
Hide file tree
Showing 21 changed files with 534 additions and 61 deletions.
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ lazy val tests = projectMatrix

lazy val complianceTests = projectMatrix
.in(file("modules/compliance-tests"))
.dependsOn(core, http4s % "test->compile", testUtils)
.dependsOn(core, http4s % "compile->compile; test->compile", testUtils)
.settings(
name := "compliance-tests",
Compile / allowedNamespaces := Seq("smithy.test", "smithy4s.example"),
Expand Down Expand Up @@ -925,7 +925,7 @@ lazy val Dependencies = new {
}

object Webjars {
val swaggerUi: ModuleID = "org.webjars.npm" % "swagger-ui-dist" % "4.14.3"
val swaggerUi: ModuleID = "org.webjars.npm" % "swagger-ui-dist" % "4.15.0"

val webjarsLocator: ModuleID = "org.webjars" % "webjars-locator" % "0.42"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.7.1
sbt.version=1.7.2
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.7.1
sbt.version=1.7.2
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.7.1
sbt.version=1.7.2
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.7.1
sbt.version=1.7.2
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.7.1
sbt.version=1.7.2
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.7.1
sbt.version=1.7.2
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.7.1
sbt.version=1.7.2
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ private[compliancetests] class CompatEffect(implicit
def deferred[A]: IO[Deferred[IO, A]] = Deferred[IO, A]

val utf8Encode: fs2.Pipe[IO, String, Byte] = fs2.text.utf8Encode[IO]
val utf8Decode: fs2.Pipe[IO, Byte, String] = fs2.text.utf8Decode[IO]
}

object Compat {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ private[compliancetests] class CompatEffect {
def deferred[A]: IO[Deferred[IO, A]] = Deferred[IO, A]

val utf8Encode: fs2.Pipe[IO, String, Byte] = fs2.text.utf8.encode[IO]
val utf8Decode: fs2.Pipe[IO, Byte, String] = fs2.text.utf8.decode[IO]
}

object Compat {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@

package smithy4s.compliancetests

import cats.implicits._
import ComplianceTest._
import org.http4s.Headers
import org.typelevel.ci.CIString
import smithy.test.{HttpResponseTestCase, HttpRequestTestCase}

case class ComplianceTest[F[_]](name: String, run: F[ComplianceResult])

Expand All @@ -35,4 +39,60 @@ object assert {
fail(s"Actual value: $actual was not equal to $expected.")
}
}

private def headersExistenceCheck(
headers: Headers,
expected: Either[Option[List[String]], Option[List[String]]]
) = {
expected match {
case Left(forbidHeaders) =>
forbidHeaders.toList.flatten.collect {
case key if headers.get(CIString(key)).isDefined =>
assert.fail(s"Header $key is forbidden in the request.")
}.combineAll
case Right(requireHeaders) =>
requireHeaders.toList.flatten.collect {
case key if headers.get(CIString(key)).isEmpty =>
assert.fail(s"Header $key is required request.")
}.combineAll
}
}
private def headersCheck(
headers: Headers,
expected: Option[Map[String, String]]
) = {
expected.toList
.flatMap(_.toList)
.map { case (key, expectedValue) =>
headers
.get(CIString(key))
.map { v =>
assert.eql[String](expectedValue, v.head.value)
}
.getOrElse(
assert.fail(s"'$key' header is missing")
)
}
.combineAll
}

object testCase {
def checkHeaders(
tc: HttpRequestTestCase,
headers: Headers
): ComplianceResult = {
assert.headersExistenceCheck(headers, Left(tc.forbidHeaders)) *>
assert.headersExistenceCheck(headers, Right(tc.requireHeaders)) *>
assert.headersCheck(headers, tc.headers)
}

def checkHeaders(
tc: HttpResponseTestCase,
headers: Headers
): ComplianceResult = {
assert.headersExistenceCheck(headers, Left(tc.forbidHeaders)) *>
assert.headersExistenceCheck(headers, Right(tc.requireHeaders)) *>
assert.headersCheck(headers, tc.headers)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,27 @@ import cats.effect.IO
import cats.effect.Resource
import cats.implicits._
import org.http4s.HttpApp
import org.http4s.headers.`Content-Type`
import org.http4s.HttpRoutes
import org.http4s.Request
import org.http4s.Response
import org.http4s.Status
import org.http4s.Uri
import org.typelevel.ci.CIString
import smithy.test._
import smithy4s.Document
import smithy4s.compliancetests.ComplianceTest.ComplianceResult
import smithy4s.http.PayloadError
import smithy4s.http.CodecAPI
import smithy4s.Document
import smithy4s.Endpoint
import smithy4s.http.PayloadError
import smithy4s.Service
import smithy4s.ShapeTag
import smithy4s.tests.DefaultSchemaVisitor

import scala.concurrent.duration._
import smithy4s.http.HttpMediaType
import org.http4s.MediaType
import org.http4s.Header

abstract class ClientHttpComplianceTestCase[
P,
Expand All @@ -53,6 +60,7 @@ abstract class ClientHttpComplianceTestCase[
private val baseUri = uri"http://localhost/"

def getClient(app: HttpApp[IO]): Resource[IO, smithy4s.Monadic[Alg, IO]]
def codecs: CodecAPI

private def matchRequest(
request: Request[IO],
Expand All @@ -66,26 +74,6 @@ abstract class ClientHttpComplianceTestCase[
}
.getOrElse(assert.success.pure[IO])

val headerAssert =
testCase.headers
.map { expectedHeaders =>
expectedHeaders
.map { case (name, value) =>
val actual = request.headers.get(CIString(name))
actual
.map { values =>
val actualValue = values.map(_.value).fold
assert.eql(value, actualValue)
}
.getOrElse(
assert.fail(s"Header $name was not found in the response.")
)
}
.toList
.combineAll
}
.getOrElse(assert.success)

val expectedUri = baseUri
.withPath(
Uri.Path.unsafeFromString(testCase.uri)
Expand All @@ -111,13 +99,14 @@ abstract class ClientHttpComplianceTestCase[
testCase.method.toLowerCase(),
request.method.name.toLowerCase()
)

List(
bodyAssert,
headerAssert.pure[IO],
uriAssert.pure[IO],
methodAssert.pure[IO]
).combineAll
val ioAsserts = bodyAssert +:
List(
assert.testCase.checkHeaders(testCase, request.headers),
uriAssert,
methodAssert
)
.map(_.pure[IO])
ioAsserts.combineAll
}

private[compliancetests] def clientRequestTest[I, E, O, SE, SO](
Expand Down Expand Up @@ -165,16 +154,147 @@ abstract class ClientHttpComplianceTestCase[
)
}

private[compliancetests] def clientResponseTest[I, E, O, SE, SO](
endpoint: Endpoint[Op, I, E, O, SE, SO],
testCase: HttpResponseTestCase,
errorSchema: Option[ErrorResponseTest[_, E]] = None
): ComplianceTest[IO] = {
def aMediatype[A](
s: smithy4s.Schema[A],
cd: CodecAPI
): HttpMediaType = {
cd.mediaType(cd.compileCodec(s))
}

type R[I_, E_, O_, SE_, SO_] = IO[O_]

val dummyInput = DefaultSchemaVisitor(endpoint.input)

ComplianceTest[IO](
name = endpoint.id.toString + "(client|response): " + testCase.id,
run = {

val buildResult
: Either[Document => IO[Throwable], Document => IO[O]] = {
errorSchema
.toLeft {
val outputDecoder = Document.Decoder.fromSchema(endpoint.output)
(doc: Document) =>
outputDecoder
.decode(doc)
.liftTo[IO]
}
.left
.map { errorInfo =>
val errorDecoder = Document.Decoder.fromSchema(errorInfo.schema)
(doc: Document) =>
errorDecoder
.decode(doc)
.liftTo[IO]
.map(errCase =>
errorInfo.errorable.unliftError(errCase.asInstanceOf[E])
)
}
}
val mediaType = aMediatype(endpoint.output, codecs)
val status = Status.fromInt(testCase.code).liftTo[IO]

status.flatMap { status =>
val app = HttpRoutes
.of[IO] { case req =>
val body: fs2.Stream[IO, Byte] =
testCase.body
.map { body =>
fs2.Stream
.emit(body)
.through(utf8Encode)
}
.getOrElse(fs2.Stream.empty)
val headers: Seq[Header.ToRaw] =
testCase.headers.toList
.flatMap(_.toList)
.map { case (key, value) =>
Header.Raw(CIString(key), value)
}
.map(Header.ToRaw.rawToRaw)
.toSeq
req.body.compile.drain.as(
Response[IO](status)
.withBodyStream(body)
.putHeaders(headers: _*)
.putHeaders(
`Content-Type`(MediaType.unsafeParse(mediaType.value))
)
)
}
.orNotFound

getClient(app).use { client =>
val doc = testCase.params.getOrElse(Document.obj())
buildResult match {
case Left(onError) =>
onError(doc).flatMap { expectedErr =>
service
.asTransformation[R](client)
.apply(endpoint.wrap(dummyInput))
.map { _ => assert.success }
.recover { case ex: Throwable =>
assert.eql(expectedErr, ex)
}
}
case Right(onOutput) =>
onOutput(doc).flatMap { expectedOutput =>
service
.asTransformation[R](client)
.apply(endpoint.wrap(dummyInput))
.map { output => assert.eql(expectedOutput, output) }
}
}
}
}
}
)
}

def allClientTests(
): List[ComplianceTest[IO]] = {
service.endpoints.flatMap { case endpoint =>
endpoint.hints
val requestTests = endpoint.hints
.get(HttpRequestTests)
.map(_.value)
.getOrElse(Nil)
.filter(_.protocol == protocolTag.id.toString())
.filter(tc => tc.appliesTo.forall(_ == AppliesTo.CLIENT))
.map(tc => clientRequestTest(endpoint, tc))

val opResponseTests = endpoint.hints
.get(HttpResponseTests)
.map(_.value)
.getOrElse(Nil)
.filter(_.protocol == protocolTag.id.toString())
.filter(tc => tc.appliesTo.forall(_ == AppliesTo.CLIENT))
.map(tc => clientResponseTest(endpoint, tc))
val errorResponseTests = endpoint.errorable.toList
.flatMap { errorrable =>
errorrable.error.alternatives.flatMap { errorAlt =>
errorAlt.instance.hints
.get(HttpResponseTests)
.toList
.flatMap(_.value)
.filter(_.protocol == protocolTag.id.toString())
.filter(tc => tc.appliesTo.forall(_ == AppliesTo.SERVER))
.map(tc =>
clientResponseTest(
endpoint,
tc,
errorSchema =
Some(ErrorResponseTest(errorAlt.instance, errorrable))
)
)
}
}

requestTests ++ opResponseTests ++ errorResponseTests
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2021-2022 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package smithy4s.compliancetests

import smithy4s.Schema
import smithy4s.Errorable

final case class ErrorResponseTest[A, E](
schema: Schema[A],
errorable: Errorable[E]
)
Loading

0 comments on commit 5bac4e9

Please sign in to comment.