Skip to content

Commit

Permalink
Fix PaginationToken Circe codecs (#752)
Browse files Browse the repository at this point in the history
* Fix PaginationToken codecs

* Add SearchServiceSpec test case

* Cleanup the test code
  • Loading branch information
pomadchin authored May 25, 2021
1 parent d60c97e commit b28fe1a
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.azavea.franklin.datamodel

import cats.syntax.either._
import com.azavea.stac4s.meta._
import eu.timepit.refined.types.numeric.PosInt
import io.circe.Error
import io.circe.generic.semiauto._
import io.circe.parser._
import io.circe.refined._
Expand Down Expand Up @@ -31,23 +33,30 @@ object PaginationToken {
}
}

implicit val dec: Decoder[PaginationToken] = deriveDecoder
implicit val enc: Encoder[PaginationToken] = deriveEncoder
val defaultDecoder: Decoder[PaginationToken] = deriveDecoder
val defaultEncoder: Encoder[PaginationToken] = deriveEncoder

val b64Encoder = Base64.getEncoder()
val b64Decoder = Base64.getDecoder()
val b64Encoder: Base64.Encoder = Base64.getEncoder
val b64Decoder: Base64.Decoder = Base64.getDecoder

def encPaginationToken(token: PaginationToken): String = b64Encoder.encodeToString(
token.asJson.noSpaces.getBytes
token.asJson(defaultEncoder).noSpaces.getBytes
)

def decPaginationToken(encoded: String): DecodeResult[PaginationToken] = {
val jsonString: String = new String(b64Decoder.decode(encoded))
val circeResult = for {
def decPaginationTokenEither(encoded: String): Either[Error, PaginationToken] = {
val jsonString = new String(b64Decoder.decode(encoded))
for {
js <- parse(jsonString)
decoded <- js.as[PaginationToken]
decoded <- js.as[PaginationToken](defaultDecoder)
} yield decoded
circeResult.toDecodeResult
}

def decPaginationToken(encoded: String): DecodeResult[PaginationToken] =
decPaginationTokenEither(encoded).toDecodeResult

implicit val paginationTokenDecoder: Decoder[PaginationToken] =
Decoder.decodeString.emap(str => decPaginationTokenEither(str).leftMap(_.getMessage))

implicit val paginationTokenEncoder: Encoder[PaginationToken] = { encPaginationToken(_).asJson }

}
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,10 @@ class TestClient[F[_]: Sync](
collection: StacCollection
): Resource[F, (StacCollection, StacItem)] =
(getCollectionResource(collection), getItemResource(collection, item)).tupled

def getCollectionItemsResource(
items: List[StacItem],
collection: StacCollection
): Resource[F, (StacCollection, List[StacItem])] =
(getCollectionResource(collection), items.traverse(getItemResource(collection, _))).tupled
}
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,13 @@ object FiltersFor {
)
filters.combineAll
}

def inclusiveFilters(collection: StacCollection): SearchFilters = {
val filters: NonEmptyList[Option[SearchFilters]] =
NonEmptyList.of(collectionFilterFor(collection).some)
val concatenated = filters.combineAll
// guaranteed to succeed, since most of the filters are being converted into options
// just to cooperate with timeFilterFor
concatenated.get
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import cats.syntax.all._
import com.azavea.franklin.Generators
import com.azavea.franklin.api.{TestClient, TestServices}
import com.azavea.franklin.database.{SearchFilters, TestDatabaseSpec}
import com.azavea.franklin.datamodel.StacSearchCollection
import com.azavea.franklin.datamodel.{PaginationToken, StacSearchCollection}
import com.azavea.stac4s.testing.JvmInstances._
import com.azavea.stac4s.testing._
import com.azavea.stac4s.{StacCollection, StacItem}
import com.azavea.stac4s.{StacCollection, StacItem, StacLinkType}
import eu.timepit.refined.types.numeric.NonNegInt
import io.circe.syntax._
import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe.CirceEntityEncoder._
import org.http4s.{Method, Request, Uri}
Expand All @@ -24,14 +26,15 @@ class SearchServiceSpec
This specification verifies that the Search Service sensibly finds and excludes items

The search service should:
- search with POST search filters $postSearchFiltersExpectation
- search with GET search filters $getSearchFiltersExpectation
- find an item with filters designed to find it $findItemWhenExpected
- not find items when excluded by time $dontFindTimeFilters
- not find items when excluded by bbox $dontFindBboxFilters
- not find items when excluded by intersection $dontFindGeomFilters
- not find items when excluded by collection $dontFindCollectionFilters
- not find items when excluded by item $dontFindItemFilters
- search with POST search filters $postSearchFiltersExpectation
- search with GET search filters $getSearchFiltersExpectation
- find an item with filters designed to find it $findItemWhenExpected
- find two items with filters designed to find it $find2ItemsWhenExpected
- not find items when excluded by time $dontFindTimeFilters
- not find items when excluded by bbox $dontFindBboxFilters
- not find items when excluded by intersection $dontFindGeomFilters
- not find items when excluded by collection $dontFindCollectionFilters
- not find items when excluded by item $dontFindItemFilters
"""
val testServices = new TestServices[IO](transactor)

Expand Down Expand Up @@ -114,6 +117,50 @@ class SearchServiceSpec
result.features.head.id should beEqualTo(stacItem.id)
}

def find2ItemsWhenExpected = prop {
(stacItem1: StacItem, stacItem2: StacItem, stacCollection: StacCollection) =>
val resourceIO = testClient map {
_.getCollectionItemsResource(stacItem1 :: stacItem2 :: Nil, stacCollection)
}
val requestIO = resourceIO flatMap { resource =>
def getSearchCollection(searchFilters: SearchFilters): IO[Option[StacSearchCollection]] = {
val request =
Request[IO](method = Method.POST, uri = Uri.unsafeFromString(s"/search"))
.withEntity(searchFilters.asJson)
(for {
resp <- testServices.searchService.routes.run(request)
decoded <- OptionT.liftF { resp.as[StacSearchCollection] }
} yield decoded).value
}

resource.use {
case (collection, _) =>
val inclusiveParams =
FiltersFor.inclusiveFilters(collection).copy(limit = NonNegInt.from(1).toOption)
val result1 = getSearchCollection(inclusiveParams)

val result2 = result1
.flatMap {
_.flatTraverse { r =>
/** This line intentionally decodes next token [[String]] into [[PaginationToken]] */
val next = r.links.collectFirst {
case l if l.rel == StacLinkType.Next =>
l.href.split("next=").last.asJson.as[PaginationToken].toOption
}.flatten
getSearchCollection(inclusiveParams.copy(next = next))
}
}

(result1, result2).tupled
}
}

val (Some(result1), Some(result2)) = requestIO.unsafeRunSync()

result1.features.head.id should beEqualTo(stacItem1.id)
result2.features.head.id should beEqualTo(stacItem2.id)
}

def dontFindTimeFilters =
getExclusionTest("temporal extent")(_ => item => FiltersFor.timeFilterExcluding(item))

Expand Down

0 comments on commit b28fe1a

Please sign in to comment.