All Downloads are FREE. Search and download functionalities are using the official Maven repository.

endpoints4s.algebra.server.EndpointsTestSuite.scala Maven / Gradle / Ivy

package endpoints4s.algebra.server

import java.time.LocalDate
import java.util.UUID
import org.apache.pekko.http.scaladsl.model.HttpMethods.{DELETE, GET, PUT}
import org.apache.pekko.http.scaladsl.model.headers.{
  Authorization,
  BasicHttpCredentials,
  ETag,
  RawHeader,
  `Access-Control-Allow-Origin`,
  `Last-Modified`
}
import org.apache.pekko.http.scaladsl.model.{DateTime, HttpMethods, HttpRequest, StatusCodes}
import endpoints4s.{Invalid, Valid}

trait EndpointsTestSuite[T <: endpoints4s.algebra.EndpointsTestApi] extends ServerTestBase[T] {

  import DecodedUrl._
  import serverApi.{segment => s, _}

  "paths" should {

    "static" in {
      decodeUrl(path)("/") shouldEqual Matched(())
      decodeUrl(path)("/foo") shouldEqual NotMatched
      decodeUrl(path / "foo")("/foo") shouldEqual Matched(())
      decodeUrl(path / "foo" / "")("/foo/") shouldEqual Matched(())
      decodeUrl(path / "foo")("/") shouldEqual NotMatched
      decodeUrl(path / "foo")("/foo/") shouldEqual NotMatched
      decodeUrl(path / "foo" / "")("/foo") shouldEqual NotMatched
      decodeUrl(path / "foo" / "bar")("/foo/bar") shouldEqual Matched(())
      decodeUrl(path / "foo" / "bar")("/foo") shouldEqual NotMatched
    }

    "decode segments" in {
      decodeUrl(path / s[Int]())("/42") shouldEqual Matched(42)
      decodeUrl(path / s[Long]())("/42") shouldEqual Matched(42L)
      decodeUrl(path / s[Double]())("/42.0") shouldEqual Matched(42.0)
      decodeUrl(path / s[Int]() / "")("/42/") shouldEqual Matched(42)
      decodeUrl(path / s[Int]() / "")("/42") shouldEqual NotMatched
      decodeUrl(path / s[Int]())("/") shouldEqual NotMatched
      decodeUrl(path / s[Int]())("/42/bar") shouldEqual NotMatched
      decodeUrl(path / s[Int]())("/foo") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'foo' for segment"
        )
      )
      decodeUrl(path / s[String]())("/foo%20bar") shouldEqual Matched("foo bar")
      decodeUrl(path / s[String]())("/foo/bar") shouldEqual NotMatched
      decodeUrl(path / s[Int]() / "baz")("/42/baz") shouldEqual Matched(42)
      decodeUrl(path / s[Int]() / "baz")("/foo/baz") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'foo' for segment"
        )
      )
      decodeUrl(path / s[Int]() / "baz")("/42") shouldEqual NotMatched
      decodeUrl(path / s[Int]() / "baz")("/foo") shouldEqual NotMatched
      decodeUrl(path / "foo" / remainingSegments())("/foo/bar%2Fbaz/quux") shouldEqual Matched(
        "bar%2Fbaz/quux"
      )
      decodeUrl(path / "foo" / remainingSegments())("/foo") shouldEqual NotMatched
    }

    "transformed" in {
      val itemId = s[String]("itemId").xmapPartial { rawId =>
        val sep = rawId.indexOf("-")
        if (sep == -1)
          Invalid(s"Invalid item id value '$rawId' for segment 'itemId'")
        else {
          val (id, name) = rawId.splitAt(sep)
          Valid(Item(name.drop(1), id))
        }
      }(item => s"${item.id}-${item.name}")
      decodeUrl(path / itemId)("/42-programming-in-scala") shouldEqual Matched(
        Item("programming-in-scala", "42")
      )
      decodeUrl(path / itemId)("/foo") shouldEqual Malformed(
        Seq(
          "Invalid item id value 'foo' for segment 'itemId'"
        )
      )

      val file = s[String]("file").xmap(new java.io.File(_))(_.getPath)
      decodeUrl(path / "assets" / file)("/assets/favicon.png") shouldEqual Matched(
        new java.io.File("favicon.png")
      )
    }

  }

  "decode query strings" should {

    "primitives" in {
      decodeUrl(path / "foo" /? qs[Int]("n"))("/foo?n=42") shouldEqual Matched(
        42
      )
      decodeUrl(path / "foo" /? qs[Long]("n"))("/foo?n=42") shouldEqual Matched(
        42L
      )
      decodeUrl(path / "foo" /? qs[String]("s"))("/foo?s=bar") shouldEqual Matched(
        "bar"
      )
      decodeUrl(path / "foo" /? qs[String]("s"))("/foo?s=bar%2Cbaz") shouldEqual Matched(
        "bar,baz"
      )
      decodeUrl(path / "foo" /? qs[Boolean]("b"))("/foo?b=true") shouldEqual Matched(
        true
      )
      decodeUrl(path / "foo" /? qs[Boolean]("b"))("/foo?b=false") shouldEqual Matched(
        false
      )
      decodeUrl(path / "foo" / "" /? qs[Int]("n"))("/foo/?n=42") shouldEqual Matched(
        42
      )
      decodeUrl(path / "foo" /? qs[Int]("n"))("/foo") shouldEqual Malformed(
        Seq(
          "Missing value for query parameter 'n'"
        )
      )
      decodeUrl(path / "foo" /? qs[Int]("n"))("/foo?n=bar") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'bar' for query parameter 'n'"
        )
      )
    }

    "optional" in {
      val url = path /? qs[Option[Int]]("n")
      decodeUrl(url)("/") shouldEqual Matched(None)
      decodeUrl(url)("/?n=42") shouldEqual Matched(Some(42))
      decodeUrl(url)("/?n=bar") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'bar' for query parameter 'n'"
        )
      )
    }

    "optional with default value" in {
      val url = path /? optQsWithDefault[Int]("n", 42)
      decodeUrl(url)("/") shouldEqual Matched(42)
      decodeUrl(url)("/?n=42") shouldEqual Matched(42)
      decodeUrl(url)("/?n=43") shouldEqual Matched(43)
      decodeUrl(url)("/?n=bar") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'bar' for query parameter 'n'"
        )
      )
    }

    "list" in {
      val url = path /? qs[List[Int]]("xs")
      decodeUrl(url)("/") shouldEqual Matched(Nil)
      decodeUrl(url)("/?xs=1&xs=2") shouldEqual Matched(1 :: 2 :: Nil)
      decodeUrl(url)("/?xs=1&xs=two") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'two' for query parameter 'xs'"
        )
      )
      decodeUrl(url)("/?xs=one&xs=two") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'one' for query parameter 'xs'",
          "Invalid integer value 'two' for query parameter 'xs'"
        )
      )
    }

    "transformed" in {
      implicit val pageQueryString: QueryStringParam[Page] =
        intQueryString.xmap(Page(_))(_.number)
      val url = path /? qs[Page]("page")
      decodeUrl(url)("/?page=42") shouldEqual Matched(Page(42))
      decodeUrl(url)("/?page=foo") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'foo' for query parameter 'page'"
        )
      )

      implicit val blogSlugQueryString: QueryStringParam[BlogSlug] =
        stringQueryString.xmap(BlogSlug(_))(_.slug)
      val url2 = path /? qs[BlogSlug]("slug")
      decodeUrl(url2)("/?slug=this-is-a-slug") shouldEqual (Matched(
        BlogSlug("this-is-a-slug")
      ))
      decodeUrl(url2)("/?slug=this%20is%20a%20slug") shouldEqual (Matched(
        BlogSlug("this is a slug")
      ))
    }

  }

  "urls" should {

    "transformed" in {
      val paginatedUrl =
        (path /? (qs[Int]("from") & qs[Int]("limit")))
          .xmap((Page2.apply _).tupled)(p => (p.from, p.limit))
      decodeUrl(paginatedUrl)("/?from=1&limit=10") shouldEqual Matched(
        Page2(1, 10)
      )
      decodeUrl(paginatedUrl)("/?from=one&limit=10") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'one' for query parameter 'from'"
        )
      )
      decodeUrl(paginatedUrl)("/?from=one&limit=ten") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'one' for query parameter 'from'",
          "Invalid integer value 'ten' for query parameter 'limit'"
        )
      )
    }

  }

  "xmap query strings" should {

    "xmap urls of locations" in {

      val locationQueryString =
        (qs[Double]("lon") & qs[Double]("lat"))
          .xmap[Location] { case (lon, lat) =>
            Location(lon, lat)
          } { location => (location.longitude, location.latitude) }
      val locationUrl = path /? locationQueryString

      decodeUrl(locationUrl)("/?lon=12.0&lat=32.9") shouldEqual Matched(
        Location(12.0, 32.9)
      )
      decodeUrl(locationUrl)("/?lon=-12.0&lat=32.9") shouldEqual Matched(
        Location(-12.0, 32.9)
      )
      decodeUrl(locationUrl)(s"/?lon=${Math.PI}&lat=-32.9") shouldEqual Matched(
        Location(Math.PI, -32.9)
      )
      decodeUrl(locationUrl)("/?lon=12&lat=32") shouldEqual Matched(
        Location(12, 32)
      )
      decodeUrl(locationUrl)("/?lat=32.0&lon=12.0") shouldEqual Matched(
        Location(12.0, 32.0)
      )

      decodeUrl(locationUrl)("/?lon=12,0&lat=32.0") shouldEqual Malformed(
        Seq(
          "Invalid number value '12,0' for query parameter 'lon'"
        )
      )
      decodeUrl(locationUrl)("/?lon=a&lat=32") shouldEqual Malformed(
        Seq(
          "Invalid number value 'a' for query parameter 'lon'"
        )
      )
      decodeUrl(locationUrl)("/?let=12.0&lat=32.0") shouldEqual Malformed(
        Seq(
          "Missing value for query parameter 'lon'"
        )
      )
      decodeUrl(locationUrl)("/?lon=12.0") shouldEqual Malformed(
        Seq(
          "Missing value for query parameter 'lat'"
        )
      )
    }

    "xmapPartial urls of blogids" in {
      val blogIdQueryString: QueryString[BlogId] =
        (qs[Option[UUID]]("uuid") & qs[Option[String]]("slug"))
          .xmapPartial[BlogId] {
            case (Some(uuid), _)    => Valid(BlogUuid(uuid))
            case (None, Some(slug)) => Valid(BlogSlug(slug))
            case (None, None) =>
              Invalid("Missing either query parameter 'uuid' or 'slug'")
          } {
            case BlogUuid(uuid) => (Some(uuid), None)
            case BlogSlug(slug) => (None, Some(slug))
          }

      val testUUID: UUID = UUID.randomUUID()
      val testMalformedUUID1: String = "f4b9defa-1ad8-453f-9a06268d"
      val testMalformedUUID2: String = "f4b9defa-1ad8-453f-9o06-2683b8564b8d"
      val testSlug: String = "test-slug"

      decodeUrl(path /? blogIdQueryString)(s"/?uuid=$testUUID") shouldEqual Matched(
        BlogUuid(testUUID)
      )
      decodeUrl(path /? blogIdQueryString)(s"/?slug=$testSlug") shouldEqual Matched(
        BlogSlug(testSlug)
      )
      decodeUrl(path /? blogIdQueryString)(s"/?uuid=$testUUID&slug=$testSlug") shouldEqual Matched(
        BlogUuid(testUUID)
      )
      decodeUrl(path /? blogIdQueryString)(s"/?slug=$testSlug&uuid=$testUUID") shouldEqual Matched(
        BlogUuid(testUUID)
      )
      decodeUrl(path /? blogIdQueryString)(s"/?slug=") shouldEqual Matched(
        BlogSlug("")
      )

      decodeUrl(path /? blogIdQueryString)(s"/?uuid=$testMalformedUUID1") shouldEqual Malformed(
        Seq(
          s"Invalid UUID value '$testMalformedUUID1' for query parameter 'uuid'"
        )
      )
      decodeUrl(path /? blogIdQueryString)(s"/?uuid=$testMalformedUUID2") shouldEqual Malformed(
        Seq(
          s"Invalid UUID value '$testMalformedUUID2' for query parameter 'uuid'"
        )
      )
      decodeUrl(path /? blogIdQueryString)(s"/") shouldEqual Malformed(
        Seq(
          "Missing either query parameter 'uuid' or 'slug'"
        )
      )
    }

  }

  "multiple errors" should {
    "be accumulated" in {
      decodeUrl(path / s[Int]() /? qs[Int]("x"))("/foo?x=bar") shouldEqual Malformed(
        Seq(
          "Invalid integer value 'foo' for segment",
          "Invalid integer value 'bar' for query parameter 'x'"
        )
      )
    }
  }

  "Server interpreter" should {

    "return server response for UUID" in {

      val uuid = UUID.randomUUID()
      val mockedResponse = "interpretedServerResponse"

      serveEndpoint(serverApi.UUIDEndpoint, mockedResponse) { port =>
        val request =
          HttpRequest(uri = s"http://localhost:$port/user/$uuid/description?name=name1&age=18")
        whenReady(sendAndDecodeEntityAsText(request)) { case (response, entity) =>
          assert(entity == mockedResponse)
          assert(response.status.intValue() == 200)
          ()
        }
      }

      serveEndpoint(serverApi.putUUIDEndpoint, ()) { port =>
        val request =
          HttpRequest(method = PUT, uri = s"http://localhost:$port/user/$uuid")
        whenReady(send(request)) { case (response, entity) =>
          assert(entity.isEmpty)
          assert(response.status.intValue() == 200)
          ()
        }
      }

      serveEndpoint(serverApi.deleteUUIDEndpoint, ()) { port =>
        val request = HttpRequest(
          method = DELETE,
          uri = s"http://localhost:$port/user/$uuid"
        )
        whenReady(send(request)) { case (response, entity) =>
          assert(entity.isEmpty)
          assert(response.status.intValue() == 200)
          ()
        }
      }
    }

    "return server response" in {

      val mockedResponse = "interpretedServerResponse"

      serveEndpoint(serverApi.smokeEndpoint, mockedResponse) { port =>
        val request =
          HttpRequest(uri = s"http://localhost:$port/user/userId/description?name=name1&age=18")
        whenReady(sendAndDecodeEntityAsText(request)) { case (response, entity) =>
          assert(entity == mockedResponse)
          assert(response.status.intValue() == 200)
          ()
        }
      }

      serveEndpoint(serverApi.putEndpoint, ()) { port =>
        val request =
          HttpRequest(method = PUT, uri = s"http://localhost:$port/user/foo123")
        whenReady(send(request)) { case (response, entity) =>
          assert(entity.isEmpty)
          assert(response.status.intValue() == 200)
          ()
        }
      }

      serveEndpoint(serverApi.putEndpointMapped, Some(())) { port =>
        val header = Authorization(BasicHttpCredentials("user", "pass"))
        val request =
          HttpRequest(
            method = PUT,
            uri = s"http://localhost:$port/user/foo123",
            headers = List(header)
          )
        whenReady(send(request)) { case (response, entity) =>
          assert(entity.isEmpty)
          assert(response.status.intValue() == 200)
          ()
        }
      }

      serveEndpoint(serverApi.deleteEndpoint, ()) { port =>
        val request =
          HttpRequest(method = DELETE, s"http://localhost:$port/user/foo123")
        whenReady(send(request)) { case (response, entity) =>
          assert(entity.isEmpty)
          assert(response.status.intValue() == 200)
          ()
        }
      }

      serveEndpoint(serverApi.trailingSlashEndpoint, ()) { port =>
        val request = HttpRequest(method = GET, s"http://localhost:$port/user/")
        whenReady(send(request)) { case (response, entity) =>
          assert(entity.isEmpty)
          assert(response.status.intValue() == 200)
          ()
        }
      }
    }

    "Handle exceptions by default" in {
      serveEndpoint(serverApi.smokeEndpoint, sys.error("Sorry.")) { port =>
        val request = HttpRequest(uri = s"http://localhost:$port/user/foo/description?name=a&age=1")
        whenReady(sendAndDecodeEntityAsText(request)) { case (response, entity) =>
          assert(response.status.intValue() == 500)
          assert(entity == "[\"Sorry.\"]")
          ()
        }
      }
    }

    "encode response headers" in {
      val entity = "foo"
      val etag = UUID.randomUUID().toString
      val lastModified = DateTime.now
      val cache =
        serverApi.Cache(s""""$etag"""", lastModified.toRfc1123DateTimeString())
      serveEndpoint(serverApi.versionedResource, (entity, cache)) { port =>
        val request =
          HttpRequest(uri = s"http://localhost:$port/versioned-resource")
        whenReady(sendAndDecodeEntityAsText(request)) { case (response, responseEntity) =>
          assert(responseEntity == entity)
          assert(response.header[ETag].contains(ETag(etag)))
          assert(
            response
              .header[`Last-Modified`]
              .contains(`Last-Modified`(lastModified))
          )
          ()
        }
      }
    }

    "encode optional response headers" in {
      val entity = "foo"
      val origin = Some("*")
      serveEndpoint(
        serverApi.endpointWithOptionalResponseHeader,
        (entity, origin)
      ) { port =>
        val request =
          HttpRequest(uri = s"http://localhost:$port/maybe-cors-enabled")
        whenReady(sendAndDecodeEntityAsText(request)) { case (response, responseEntity) =>
          assert(responseEntity == entity)
          assert(
            response
              .header[`Access-Control-Allow-Origin`]
              .contains(`Access-Control-Allow-Origin`.*)
          )
          ()
        }
      }
    }

    "skip missing optional response headers" in {
      val entity = "foo"
      serveEndpoint(
        serverApi.endpointWithOptionalResponseHeader,
        (entity, None)
      ) { port =>
        val request =
          HttpRequest(uri = s"http://localhost:$port/maybe-cors-enabled")
        whenReady(sendAndDecodeEntityAsText(request)) { case (response, responseEntity) =>
          assert(responseEntity == entity)
          assert(response.header[`Access-Control-Allow-Origin`].isEmpty)
          ()
        }
      }
    }

    "reject requests with two missing headers" in {
      serveEndpoint(joinedHeadersEndpoint, "ignored") { port =>
        val noHeadersRequest =
          HttpRequest(uri = s"http://localhost:$port/joinedHeadersEndpoint")
        whenReady(sendAndDecodeEntityAsText(noHeadersRequest)) { case (response, entity) =>
          assert(response.status == StatusCodes.BadRequest)
          assert(entity == """["Missing header A","Missing header B"]""")
        }
        ()
      }
    }

    "reject requests with one missing header" in {
      serveEndpoint(joinedHeadersEndpoint, "ignored") { port =>
        val oneHeader =
          HttpRequest(uri = s"http://localhost:$port/joinedHeadersEndpoint")
            .withHeaders(RawHeader("A", "foo"))
        whenReady(sendAndDecodeEntityAsText(oneHeader)) { case (response, entity) =>
          assert(response.status == StatusCodes.BadRequest)
          assert(entity == """["Missing header B"]""")
        }
        ()
      }
    }

    "accept requests with the required headers" in {
      serveEndpoint(joinedHeadersEndpoint, "success") { port =>
        val twoHeaders =
          HttpRequest(uri = s"http://localhost:$port/joinedHeadersEndpoint")
            .withHeaders(RawHeader("A", "foo"), RawHeader("B", "foo"))
        whenReady(sendAndDecodeEntityAsText(twoHeaders)) { case (response, entity) =>
          assert(response.status == StatusCodes.OK)
          assert(entity == "success")
        }
        ()
      }
    }

    "accept requests with the required case insensitive headers" in {
      serveEndpoint(joinedHeadersEndpoint, "success") { port =>
        val twoHeaders =
          HttpRequest(uri = s"http://localhost:$port/joinedHeadersEndpoint")
            .withHeaders(RawHeader("a", "foo"), RawHeader("b", "foo"))
        whenReady(sendAndDecodeEntityAsText(twoHeaders)) { case (response, entity) =>
          assert(response.status == StatusCodes.OK)
          assert(entity == "success")
        }
        ()
      }
    }

    "decode transformed request headers" in {
      serveEndpoint(xmapHeadersEndpoint, "ignored") { port =>
        val validRequest =
          HttpRequest(uri = s"http://localhost:$port/xmapHeadersEndpoint")
            .withHeaders(RawHeader("C", "42"))
        whenReady(sendAndDecodeEntityAsText(validRequest)) { case (response, _) =>
          assert(response.status == StatusCodes.OK)
        }
        val invalidRequest =
          HttpRequest(uri = s"http://localhost:$port/xmapHeadersEndpoint")
            .withHeaders(RawHeader("C", "forty-two"))
        whenReady(sendAndDecodeEntityAsText(invalidRequest)) { case (response, entity) =>
          assert(response.status == StatusCodes.BadRequest)
          assert(entity == """["Invalid integer: forty-two"]""")
        }
        ()
      }
    }

    "decode transformed request entities" in {
      serveEndpoint(xmapReqBodyEndpoint, "ignored") { port =>
        val validRequest =
          HttpRequest(
            uri = s"http://localhost:$port/xmapReqBodyEndpoint",
            method = HttpMethods.POST
          ).withEntity(LocalDate.now().format(dateTimeFormatter))
        whenReady(sendAndDecodeEntityAsText(validRequest)) { case (response, _) =>
          assert(response.status == StatusCodes.OK)
        }
        val invalidRequest =
          HttpRequest(
            uri = s"http://localhost:$port/xmapReqBodyEndpoint",
            method = HttpMethods.POST
          ).withEntity("not a date")
        whenReady(sendAndDecodeEntityAsText(invalidRequest)) { case (response, entity) =>
          assert(response.status == StatusCodes.BadRequest)
          assert(entity == """["Invalid date value 'not a date'"]""")
        }
        ()
      }
    }

    "decode transformed request" in {
      serveEndpoint(endpointWithTransformedRequest, ()) { port =>
        val validRequest =
          HttpRequest(uri = s"http://localhost:$port/transformed-request?n=9")
            .withHeaders(RawHeader("Accept", "text/html"))
        whenReady(sendAndDecodeEntityAsText(validRequest)) { case (response, _) =>
          assert(response.status == StatusCodes.OK)
        }
        val invalidRequest =
          HttpRequest(uri = s"http://localhost:$port/transformed-request?n=10")
            .withHeaders(RawHeader("Accept", "text/html"))
        whenReady(sendAndDecodeEntityAsText(invalidRequest)) { case (response, entity) =>
          assert(response.status == StatusCodes.BadRequest)
          assert(
            entity == """["Invalid combination of request header and query string parameter"]"""
          )
        }
        ()
      }
    }

    "encode transformed response entities" in {
      val entity = StringWrapper("foo")
      serveEndpoint(
        serverApi.endpointWithTransformedResponseEntity,
        entity
      ) { port =>
        val request =
          HttpRequest(uri = s"http://localhost:$port/transformed-response-entity")
        whenReady(sendAndDecodeEntityAsText(request)) { case (_, responseEntity) =>
          assert(responseEntity == entity.str)
        }
        ()
      }
    }

    "encode transformed response" in {
      val resp = TransformedResponse("foo", "\"42\"")
      serveEndpoint(
        serverApi.endpointWithTransformedResponse,
        resp
      ) { port =>
        val request =
          HttpRequest(uri = s"http://localhost:$port/transformed-response")
        whenReady(sendAndDecodeEntityAsText(request)) { case (response, responseEntity) =>
          assert(responseEntity == resp.entity)
          assert(response.headers[ETag].contains(ETag("42")))
        }
        ()
      }
    }

    "handle mapped endpoints" in {
      serveEndpoint(serverApi.mappedEndpoint, Left(())) { port =>
        // empty request
        val request1 =
          HttpRequest(method = GET, uri = s"http://localhost:$port/mapped")
        whenReady(sendAndDecodeEntityAsText(request1)) { case (response, entity) =>
          assert(response.status.intValue() == 400)
          val errors = ujson.read(entity).arr.map(_.str)
          val expectedErrors =
            Seq(
              "Missing header If-Modified-Since",
              "Missing header If-None-Match",
              "Missing value for query parameter 'x'",
              "Missing value for query parameter 'y'"
            )
          assert(
            expectedErrors.forall(errors.contains(_)),
            s"Missing errors: ${expectedErrors.diff(errors)}"
          )
          ()
        }
        // happy path
        val request2 =
          HttpRequest(method = GET, uri = s"http://localhost:$port/mapped?x=1&y=2")
            .withHeaders(
              RawHeader("If-None-Match", "\"xxx\""),
              RawHeader("If-Modified-Since", "Wed, 21 Oct 2015 07:28:00 GMT")
            )
        whenReady(sendAndDecodeEntityAsText(request2)) { case (response, entity) =>
          assert(response.status.intValue() == 304)
          assert(entity == "")
          ()
        }
      }

      serveEndpoint(
        serverApi.mappedEndpoint,
        Right(("\"yyy\"", "Wed, 21 Oct 2015 17:28:00 GMT"))
      ) { port =>
        // happy path
        val request =
          HttpRequest(method = GET, uri = s"http://localhost:$port/mapped?x=1&y=2")
            .withHeaders(
              RawHeader("If-None-Match", "\"xxx\""),
              RawHeader("If-Modified-Since", "Wed, 21 Oct 2015 07:28:00 GMT")
            )
        whenReady(sendAndDecodeEntityAsText(request)) { case (response, entity) =>
          assert(response.status.intValue() == 200)
          assert(response.headers.contains(ETag("yyy")))
          assert(response.headers.contains(`Last-Modified`(DateTime(2015, 10, 21, 17, 28, 0))))
          assert(entity == "")
          ()
        }
      }

    }

  }
}

case class Item(name: String, id: String)
case class Page(number: Int)
case class Page2(from: Int, limit: Int)
case class Location(longitude: Double, latitude: Double)
sealed trait BlogId
case class BlogUuid(uuid: UUID) extends BlogId
case class BlogSlug(slug: String) extends BlogId




© 2015 - 2024 Weber Informatics LLC | Privacy Policy