Extend /v1/parties endpoint to support fetch by party IDs (#4680)

* Extend /party endpoint to allow specifying party ids

* Extend /party endpoint to allow specifying party ids

* Update docs

CHANGELOG_BEGIN

[JSON API - Experimental] Fetch Parties by their Identifiers. See #4512
``/v1/parties`` endpoint supports POST method now, which expects
a JSON array of party identifiers as an input.

CHANGELOG_END

* minor update

* minor update

* Use type alias

* Add warnings to the sync response

* test cases

* update docs, add test case for an empty input

* cleanup

* cleanup

* Addressing code review comments
This commit is contained in:
Leonid Shlyapnikov 2020-02-26 10:24:01 -05:00 committed by GitHub
parent 8c14d16718
commit d58bb4597e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 345 additions and 91 deletions

View File

@ -803,12 +803,28 @@ Nonempty HTTP Response with Unknown Template IDs Warning
"status": 200
}
Fetch All Known Parties
***********************
Fetch Parties by Identifiers
****************************
- URL: ``/v1/parties``
- Method: ``GET``
- Content: <EMPTY>
- Method: ``POST``
- Content-Type: ``application/json``
- Content:
.. code-block:: json
["Alice", "Bob", "Dave"]
If empty JSON array is passed: ``[]``, this endpoint returns BadRequest(400) error:
.. code-block:: json
{
"status": 400,
"errors": [
"JsonReaderError. Cannot read JSON: <[]>. Cause: spray.json.DeserializationException: must be a list with at least 1 element"
]
}
HTTP Response
=============
@ -819,15 +835,69 @@ HTTP Response
.. code-block:: json
{
"status": 200,
"result": [
{
"party": "Alice",
"isLocal": true
}
]
"status": 200,
"result": [
{
"identifier": "Alice",
"displayName": "Alice & Co. LLC",
"isLocal": true
},
{
"identifier": "Bob",
"displayName": "Bob & Co. LLC",
"isLocal": true
},
{
"identifier": "Dave",
"isLocal": true
}
]
}
Please note that the order of the party objects in the response is not guaranteed to match the order of the passed party identifiers.
Where
- ``identifier`` -- a stable unique identifier of a DAML party,
- ``displayName`` -- optional human readable name associated with the party. Might not be unique,
- ``isLocal`` -- true if party is hosted by the backing participant.
HTTP Response with Unknown Parties Warning
============================================
- Content-Type: ``application/json``
- Content:
.. code-block:: json
{
"result": [
{
"identifier": "Alice",
"displayName": "Alice & Co. LLC",
"isLocal": true
}
],
"warnings": {
"unknownParties": [
"Erin"
]
},
"status": 200
}
Fetch All Known Parties
***********************
- URL: ``/v1/parties``
- Method: ``GET``
- Content: <EMPTY>
HTTP Response
=============
The response is the same as for the POST method above.
Streaming API
*************

View File

@ -15,6 +15,7 @@ import com.digitalasset.http.EndpointsCompanion._
import com.digitalasset.http.Statement.discard
import com.digitalasset.http.domain.JwtPayload
import com.digitalasset.http.json._
import com.digitalasset.http.util.Collections.toNonEmptySet
import com.digitalasset.http.util.FutureUtil.{either, eitherT, rightT}
import com.digitalasset.http.util.{ApiValueToLfValueConverter, FutureUtil}
import com.digitalasset.jwt.domain.Jwt
@ -23,14 +24,16 @@ import com.digitalasset.ledger.api.{v1 => lav1}
import com.digitalasset.util.ExceptionOps._
import com.typesafe.scalalogging.StrictLogging
import scalaz.std.scalaFuture._
import scalaz.syntax.bitraverse._
import scalaz.syntax.show._
import scalaz.syntax.std.option._
import scalaz.syntax.traverse._
import scalaz.{-\/, EitherT, Show, \/, \/-}
import scalaz.{-\/, Bitraverse, EitherT, NonEmptyList, Show, \/, \/-}
import spray.json._
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future}
import scala.language.higherKinds
import scala.util.control.NonFatal
@SuppressWarnings(Array("org.wartremover.warts.Any"))
@ -56,10 +59,11 @@ class Endpoints(
case req @ HttpRequest(POST, Uri.Path("/v1/fetch"), _, _, _) => httpResponse(fetch(req))
case req @ HttpRequest(GET, Uri.Path("/v1/query"), _, _, _) => httpResponse(retrieveAll(req))
case req @ HttpRequest(POST, Uri.Path("/v1/query"), _, _, _) => httpResponse(query(req))
case req @ HttpRequest(GET, Uri.Path("/v1/parties"), _, _, _) => httpResponse(parties(req))
case req @ HttpRequest(GET, Uri.Path("/v1/parties"), _, _, _) => httpResponse(allParties(req))
case req @ HttpRequest(POST, Uri.Path("/v1/parties"), _, _, _) => httpResponse(parties(req))
}
def create(req: HttpRequest): ET[JsValue] =
def create(req: HttpRequest): ET[domain.OkResponse[JsValue, Unit]] =
for {
t3 <- FutureUtil.eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
@ -77,9 +81,9 @@ class Endpoints(
jsVal <- either(encoder.encodeV(ac).leftMap(e => ServerError(e.shows))): ET[JsValue]
} yield jsVal
} yield domain.OkResponse(jsVal)
def exercise(req: HttpRequest): ET[JsValue] =
def exercise(req: HttpRequest): ET[domain.OkResponse[JsValue, Unit]] =
for {
t3 <- eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
@ -107,11 +111,11 @@ class Endpoints(
jsResp <- either(lfResp.traverse(lfValueToJsValue)): ET[domain.ExerciseResponse[JsValue]]
jsVal <- either(SprayJson.encode(jsResp).leftMap(e => ServerError(e.shows))): ET[JsValue]
jsVal <- either(toJsValue(jsResp)): ET[JsValue]
} yield jsVal
} yield domain.OkResponse(jsVal)
def fetch(req: HttpRequest): ET[JsValue] =
def fetch(req: HttpRequest): ET[domain.OkResponse[JsValue, Unit]] =
for {
input <- FutureUtil.eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
@ -135,7 +139,7 @@ class Endpoints(
ac.cata(x => lfAcToJsValue(x).leftMap(e => ServerError(e.shows)), \/-(JsNull))
): ET[JsValue]
} yield jsVal
} yield domain.OkResponse(jsVal)
def retrieveAll(req: HttpRequest): Future[Error \/ SearchResult[Error \/ JsValue]] =
input(req).map {
@ -167,19 +171,45 @@ class Endpoints(
val jsValSource: Source[Error \/ JsValue, NotUsed] = result.source
.via(handleSourceFailure)
.map(_.flatMap(jsAcToJsValue))
.map(_.flatMap(toJsValue[domain.ActiveContract[JsValue]](_)))
result.copy(source = jsValSource): SearchResult[Error \/ JsValue]
}
}
}
def parties(req: HttpRequest): ET[JsValue] =
def allParties(req: HttpRequest): ET[domain.OkResponse[JsValue, JsValue]] =
for {
_ <- FutureUtil.eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
ps <- rightT(partiesService.allParties()): ET[List[domain.PartyDetails]]
jsVal <- either(SprayJson.encode(ps)).leftMap(e => ServerError(e.shows)): ET[JsValue]
} yield jsVal
t3 <- eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
ps <- rightT(partiesService.allParties(t3._1)): ET[List[domain.PartyDetails]]
resp = domain.OkResponse(ps, Option.empty[Unit])
result <- either(resp.bitraverse(toJsValue(_), toJsValue(_)))
} yield result
def parties(req: HttpRequest): ET[domain.OkResponse[JsValue, JsValue]] =
for {
t3 <- eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
(jwt, _, reqBody) = t3
cmd <- either(
SprayJson
.decode[NonEmptyList[domain.Party]](reqBody)
.leftMap(e => InvalidUserInput(e.shows))
): ET[NonEmptyList[domain.Party]]
ps <- eitherT(
handleFutureFailure(
partiesService.parties(jwt, toNonEmptySet(cmd))
)): ET[(Set[domain.PartyDetails], Set[domain.Party])]
resp: domain.OkResponse[List[domain.PartyDetails], domain.UnknownParties] = okResponse(
ps._1.toList,
ps._2.toList)
result <- either(resp.bitraverse(toJsValue(_), toJsValue(_)))
} yield result
private def handleFutureFailure[A: Show, B](fa: Future[A \/ B]): Future[ServerError \/ B] =
fa.map(a => a.leftMap(e => ServerError(e.shows))).recover {
@ -204,17 +234,6 @@ class Endpoints(
-\/(ServerError(e.description))
}
private def httpResponse(output: ET[JsValue]): Future[HttpResponse] = {
val fa: Future[Error \/ JsValue] = output.run
fa.map {
case \/-(a) => httpResponseOk(a)
case -\/(e) => httpResponseError(e)
}
.recover {
case NonFatal(e) => httpResponseError(ServerError(e.description))
}
}
private def httpResponse(
output: Future[Error \/ SearchResult[Error \/ JsValue]]): Future[HttpResponse] =
output
@ -245,6 +264,21 @@ class Endpoints(
)
}
private def httpResponse[A: JsonWriter, B: JsonWriter](
result: ET[domain.OkResponse[A, B]]
): Future[HttpResponse] = {
val fa: Future[Error \/ JsValue] = result.flatMap(x => either(toJsValueWithBitraverse(x))).run
fa.map {
case -\/(e) =>
httpResponseError(e)
case \/-(r) =>
HttpResponse(entity = HttpEntity.Strict(ContentTypes.`application/json`, format(r)))
}
.recover {
case NonFatal(e) => httpResponseError(ServerError(e.description))
}
}
private[http] def input(req: HttpRequest): Future[Unauthorized \/ (Jwt, JwtPayload, String)] = {
findJwt(req).flatMap(decodeAndParsePayload(_, decodeJwt)) match {
case e @ -\/(_) =>
@ -304,10 +338,30 @@ object Endpoints {
private def lfAcToJsValue(a: domain.ActiveContract[LfValue]): Error \/ JsValue = {
for {
b <- a.traverse(lfValueToJsValue): Error \/ domain.ActiveContract[JsValue]
c <- SprayJson.encode(b).leftMap(e => ServerError(e.shows))
c <- toJsValue(b)
} yield c
}
private def jsAcToJsValue(a: domain.ActiveContract[JsValue]): Error \/ JsValue =
private def okResponse(parties: List[domain.PartyDetails], unknownParties: List[domain.Party])
: domain.OkResponse[List[domain.PartyDetails], domain.UnknownParties] = {
if (unknownParties.isEmpty) domain.OkResponse(parties, Option.empty[domain.UnknownParties])
else domain.OkResponse(parties, Some(domain.UnknownParties(unknownParties)))
}
private def toJsValue[A: JsonWriter](a: A): Error \/ JsValue =
SprayJson.encode(a).leftMap(e => ServerError(e.shows))
@SuppressWarnings(Array("org.wartremover.warts.Any"))
private def toJsValueWithBitraverse[F[_, _], A, B](fab: F[A, B])(
implicit ev1: Bitraverse[F],
ev2: JsonWriter[F[JsValue, JsValue]],
ev3: JsonWriter[A],
ev4: JsonWriter[B]): Error \/ JsValue =
for {
fjj <- fab.bitraverse(
a => toJsValue(a),
b => toJsValue(b)
): Error \/ F[JsValue, JsValue]
jsVal <- toJsValue(fjj)
} yield jsVal
}

View File

@ -19,7 +19,7 @@ import com.digitalasset.http.json.{
ApiValueToJsValueConverter,
DomainJsonDecoder,
DomainJsonEncoder,
JsValueToApiValueConverter,
JsValueToApiValueConverter
}
import com.digitalasset.http.util.ApiValueToLfValueConverter
import com.digitalasset.util.ExceptionOps._
@ -32,7 +32,7 @@ import com.digitalasset.ledger.client.LedgerClient
import com.digitalasset.ledger.client.configuration.{
CommandClientConfiguration,
LedgerClientConfiguration,
LedgerIdRequirement,
LedgerIdRequirement
}
import com.digitalasset.ledger.client.services.pkg.PackageClient
import com.digitalasset.ledger.service.LedgerReader
@ -126,7 +126,7 @@ object HttpService extends StrictLogging {
contractDao,
)
partiesService = new PartiesService(() => client.partyManagementClient.listKnownParties())
partiesService = new PartiesService(LedgerClientJwt.listKnownParties(client))
(encoder, decoder) = buildJsonCodecs(ledgerId, packageService)

View File

@ -6,6 +6,7 @@ package com.digitalasset.http
import akka.NotUsed
import akka.stream.scaladsl.Source
import com.digitalasset.jwt.domain.Jwt
import com.digitalasset.ledger.api
import com.digitalasset.ledger.api.v1.active_contracts_service.GetActiveContractsResponse
import com.digitalasset.ledger.api.v1.command_service.{
SubmitAndWaitForTransactionResponse,
@ -36,6 +37,9 @@ object LedgerClientJwt {
type GetCreatesAndArchivesSince =
(Jwt, TransactionFilter, LedgerOffset, Terminates) => Source[Transaction, NotUsed]
type ListKnownParties =
Jwt => Future[List[api.domain.PartyDetails]]
private def bearer(jwt: Jwt): Some[String] = Some(s"Bearer ${jwt.value: String}")
def submitAndWaitForTransaction(client: LedgerClient): SubmitAndWaitForTransaction =
@ -97,4 +101,7 @@ object LedgerClientJwt {
case _ => false
}
}
def listKnownParties(client: LedgerClient): ListKnownParties =
jwt => client.partyManagementClient.listKnownParties(bearer(jwt))
}

View File

@ -3,14 +3,48 @@
package com.digitalasset.http
import com.digitalasset.ledger.api.domain.PartyDetails
import com.digitalasset.jwt.domain.Jwt
import com.digitalasset.ledger.api
import scalaz.OneAnd
import scala.collection.breakOut
import scala.concurrent.{ExecutionContext, Future}
class PartiesService(listAllParties: () => Future[List[PartyDetails]])(
class PartiesService(listAllParties: LedgerClientJwt.ListKnownParties)(
implicit ec: ExecutionContext) {
def allParties(): Future[List[domain.PartyDetails]] = {
listAllParties().map(ps => ps.map(p => domain.PartyDetails.fromLedgerApi(p)))
// TODO(Leo) memoize this calls or listAllParties()?
def allParties(jwt: Jwt): Future[List[domain.PartyDetails]] = {
listAllParties(jwt).map(ps => ps.map(p => domain.PartyDetails.fromLedgerApi(p)))
}
// TODO(Leo) memoize this calls or listAllParties()?
def parties(
jwt: Jwt,
identifiers: OneAnd[Set, domain.Party]
): Future[(Set[domain.PartyDetails], Set[domain.Party])] = {
val requested: Set[domain.Party] = identifiers.tail + identifiers.head
val strIds: Set[String] = domain.Party.unsubst(requested)
listAllParties(jwt).map { ps =>
val result: Set[domain.PartyDetails] = collectParties(ps, strIds)
(result, findUnknownParties(result, requested))
}
}
private def collectParties(
xs: List[api.domain.PartyDetails],
requested: Set[String]
): Set[domain.PartyDetails] =
xs.collect {
case p if requested(p.party) => domain.PartyDetails.fromLedgerApi(p)
}(breakOut)
private def findUnknownParties(
found: Set[domain.PartyDetails],
requested: Set[domain.Party]
): Set[domain.Party] =
if (found.size == requested.size) Set.empty[domain.Party]
else requested -- found.map(_.identifier)
}

View File

@ -83,7 +83,7 @@ object domain {
queries: NonEmptyList[GetActiveContractsRequest]
)
case class PartyDetails(party: Party, displayName: Option[String], isLocal: Boolean)
case class PartyDetails(identifier: Party, displayName: Option[String], isLocal: Boolean)
final case class CommandMeta(
commandId: Option[CommandId],
@ -492,7 +492,7 @@ object domain {
final case class OkResponse[R, W](
result: R,
warnings: Option[W],
warnings: Option[W] = Option.empty[W],
status: StatusCode = StatusCodes.OK,
) extends ServiceResponse
@ -521,4 +521,6 @@ object domain {
final case class UnknownTemplateIds(unknownTemplateIds: List[TemplateId.OptionalPkg])
extends ServiceWarning
final case class UnknownParties(unknownParties: List[domain.Party]) extends ServiceWarning
}

View File

@ -146,14 +146,6 @@ object JsonProtocol extends DefaultJsonProtocol {
deserializationError(s"$what requires either key or contractId field")
}
// implicit val ContractLookupRequestFormat
// : RootJsonReader[domain.ContractLookupRequest[JsValue]] = {
// case JsObject(fields) =>
// val id = decodeContractRef(fields, "ContractLookupRequest")
// domain.ContractLookupRequest(id)
// case _ => deserializationError("ContractLookupRequest must be an object")
// }
implicit val EnrichedContractKeyFormat: RootJsonFormat[domain.EnrichedContractKey[JsValue]] =
jsonFormat2(domain.EnrichedContractKey.apply[JsValue])
@ -288,8 +280,8 @@ object JsonProtocol extends DefaultJsonProtocol {
override def write(obj: StatusCode): JsValue = JsNumber(obj.intValue)
}
implicit val OkResponseFormat: RootJsonFormat[domain.OkResponse[JsValue, JsValue]] = jsonFormat3(
domain.OkResponse[JsValue, JsValue])
implicit def OkResponseFormat[A: JsonFormat, B: JsonFormat]
: RootJsonFormat[domain.OkResponse[A, B]] = jsonFormat3(domain.OkResponse[A, B])
implicit val ErrorResponseFormat: RootJsonFormat[domain.ErrorResponse[JsValue]] = jsonFormat2(
domain.ErrorResponse[JsValue])
@ -299,15 +291,22 @@ object JsonProtocol extends DefaultJsonProtocol {
override def read(json: JsValue): domain.ServiceWarning = json match {
case JsObject(fields) if fields.contains("unknownTemplateIds") =>
UnknownTemplateIdsFormat.read(json)
case JsObject(fields) if fields.contains("unknownParties") =>
UnknownPartiesFormat.read(json)
case _ =>
deserializationError(s"Expected JsObject(unknownTemplateIds -> JsArray(...)), got: $json")
deserializationError(
s"Expected JsObject(unknownTemplateIds | unknownParties -> JsArray(...)), got: $json")
}
override def write(obj: domain.ServiceWarning): JsValue = obj match {
case x: domain.UnknownTemplateIds => UnknownTemplateIdsFormat.write(x)
case x: domain.UnknownParties => UnknownPartiesFormat.write(x)
}
}
implicit val UnknownTemplateIdsFormat: RootJsonFormat[domain.UnknownTemplateIds] = jsonFormat1(
domain.UnknownTemplateIds)
implicit val UnknownPartiesFormat: RootJsonFormat[domain.UnknownParties] = jsonFormat1(
domain.UnknownParties)
}

View File

@ -4,9 +4,13 @@
package com.digitalasset.http.json
import com.digitalasset.util.ExceptionOps._
import scalaz.{-\/, Show, \/, \/-}
import scalaz.syntax.bitraverse._
import scalaz.syntax.traverse._
import scalaz.{-\/, Bitraverse, Show, Traverse, \/, \/-}
import spray.json.{JsValue, JsonReader, _}
import scala.language.higherKinds
@SuppressWarnings(Array("org.wartremover.warts.Any"))
object SprayJson {
sealed abstract class Error extends Product with Serializable
@ -44,6 +48,25 @@ object SprayJson {
def decode[A: JsonReader](a: JsValue): JsonReaderError \/ A =
\/.fromTryCatchNonFatal(a.convertTo[A]).leftMap(e => JsonReaderError(a.toString, e.description))
def decode1[F[_], A](a: JsValue)(
implicit ev1: JsonReader[F[JsValue]],
ev2: Traverse[F],
ev3: JsonReader[A]): JsonReaderError \/ F[A] =
for {
fj <- decode[F[JsValue]](a)
fa <- fj.traverse(decode[A](_))
} yield fa
def decode2[F[_, _], A, B](a: JsValue)(
implicit ev1: JsonReader[F[JsValue, JsValue]],
ev2: Bitraverse[F],
ev3: JsonReader[A],
ev4: JsonReader[B]): JsonReaderError \/ F[A, B] =
for {
fjj <- decode[F[JsValue, JsValue]](a)
fab <- fjj.bitraverse(decode[A](_), decode[B](_))
} yield fab
def encode[A: JsonWriter](a: A): JsonWriterError \/ JsValue = {
import spray.json._
\/.fromTryCatchNonFatal(a.toJson).leftMap(e => JsonWriterError(a, e.description))

View File

@ -3,7 +3,7 @@
package com.digitalasset.http.util
import scalaz.{NonEmptyList, \/}
import scalaz.{NonEmptyList, OneAnd, \/}
import scala.collection.TraversableLike
@ -31,4 +31,9 @@ object Collections {
def collect[B](f: A PartialFunction B): Option[NonEmptyList[B]] =
self.list.collect(f).toNel
}
def toNonEmptySet[A](as: NonEmptyList[A]): OneAnd[Set, A] = {
import scalaz.syntax.foldable._
OneAnd(as.head, as.tail.toSet - as.head)
}
}

View File

@ -18,7 +18,7 @@ import com.digitalasset.grpc.adapter.{AkkaExecutionSequencerPool, ExecutionSeque
import com.digitalasset.http.HttpServiceTestFixture.jsonCodecs
import com.digitalasset.http.domain.ContractId
import com.digitalasset.http.domain.TemplateId.OptionalPkg
import com.digitalasset.http.json.SprayJson.objectField
import com.digitalasset.http.json.SprayJson.{decode2, objectField}
import com.digitalasset.http.json._
import com.digitalasset.http.util.ClientUtil.boxedRecord
import com.digitalasset.http.util.FutureUtil.toFuture
@ -83,11 +83,15 @@ trait AbstractHttpServiceIntegrationTestFuns extends StrictLogging {
import shapeless.tag, tag.@@ // used for subtyping to make `AHS ec` beat executionContext
implicit val `AHS ec`: ExecutionContext @@ this.type = tag[this.type](`AHS asys`.dispatcher)
protected def withHttpService[A]
: ((Uri, DomainJsonEncoder, DomainJsonDecoder) => Future[A]) => Future[A] =
protected def withHttpServiceAndClient[A]
: ((Uri, DomainJsonEncoder, DomainJsonDecoder, LedgerClient) => Future[A]) => Future[A] =
HttpServiceTestFixture
.withHttpService[A](testId, List(dar1, dar2), jdbcConfig, staticContentConfig)
protected def withHttpService[A](
f: (Uri, DomainJsonEncoder, DomainJsonDecoder) => Future[A]): Future[A] =
withHttpServiceAndClient((a, b, c, _) => f(a, b, c))
protected def withLedger[A]: (LedgerClient => Future[A]) => Future[A] =
HttpServiceTestFixture.withLedger[A](List(dar1, dar2), testId)
@ -736,31 +740,86 @@ abstract class AbstractHttpServiceIntegrationTest
}: Future[Assertion]
}
"parties endpoint should return all known parties" in withHttpService { (uri, encoder, _) =>
val create: domain.CreateCommand[v.Record] = iouCreateCommand()
postCreateCommand(create, encoder, uri)
.flatMap {
case (createStatus, createOutput) =>
createStatus shouldBe StatusCodes.OK
assertStatus(createOutput, StatusCodes.OK)
getRequest(uri = uri.withPath(Uri.Path("/v1/parties")))
.flatMap {
case (status, output) =>
status shouldBe StatusCodes.OK
assertStatus(output, StatusCodes.OK)
inside(output) {
case JsObject(fields) =>
inside(fields.get("result")) {
case Some(jsArray) =>
inside(SprayJson.decode[List[domain.PartyDetails]](jsArray)) {
case \/-(partyDetails) =>
val partyNames: Set[String] =
partyDetails.map(_.party.unwrap)(breakOut)
partyNames should contain("Alice")
}
}
}
}
"parties endpoint should return all known parties" in withHttpServiceAndClient {
(uri, _, _, client) =>
import scalaz.std.vector._
val partyIds = Vector("Alice", "Bob", "Charlie", "Dave")
val partyManagement = client.partyManagementClient
partyIds
.traverse { p =>
partyManagement.allocateParty(Some(p), Some(s"$p & Co. LLC"))
}
.flatMap { allocatedParties =>
getRequest(uri = uri.withPath(Uri.Path("/v1/parties"))).flatMap {
case (status, output) =>
status shouldBe StatusCodes.OK
inside(
decode2[domain.OkResponse, List[domain.PartyDetails], Unit](output)
) {
case \/-(response) =>
response.status shouldBe StatusCodes.OK
response.warnings shouldBe None
val actualIds: Set[domain.Party] = response.result.map(_.identifier)(breakOut)
actualIds shouldBe domain.Party.subst(partyIds.toSet)
response.result.toSet shouldBe
allocatedParties.toSet.map(domain.PartyDetails.fromLedgerApi)
}
}
}: Future[Assertion]
}
"parties endpoint should return only requested parties, unknown parties returned as warnings" in withHttpServiceAndClient {
(uri, _, _, client) =>
import scalaz.std.vector._
val charlie = domain.Party("Charlie")
val knownParties = domain.Party.subst(Vector("Alice", "Bob", "Dave")) :+ charlie
val erin = domain.Party("Erin")
val requestedPartyIds: Vector[domain.Party] = knownParties.filterNot(_ == charlie) :+ erin
val partyManagement = client.partyManagementClient
knownParties
.traverse { p =>
partyManagement.allocateParty(Some(p.unwrap), Some(s"${p.unwrap} & Co. LLC"))
}
.flatMap { allocatedParties =>
postJsonRequest(
uri = uri.withPath(Uri.Path("/v1/parties")),
JsArray(requestedPartyIds.map(x => JsString(x.unwrap)))
).flatMap {
case (status, output) =>
status shouldBe StatusCodes.OK
inside(
decode2[domain.OkResponse, List[domain.PartyDetails], domain.UnknownParties](output)
) {
case \/-(response) =>
response.status shouldBe StatusCodes.OK
response.warnings shouldBe Some(domain.UnknownParties(List(erin)))
val actualIds: Set[domain.Party] = response.result.map(_.identifier)(breakOut)
actualIds shouldBe requestedPartyIds.toSet - erin // Erin is not known
val expected: Set[domain.PartyDetails] = allocatedParties.toSet
.map(domain.PartyDetails.fromLedgerApi)
.filterNot(_.identifier == charlie)
response.result.toSet shouldBe expected
}
}
}: Future[Assertion]
}
"parties endpoint should error if empty array passed as input" in withHttpServiceAndClient {
(uri, _, _, _) =>
postJsonRequest(
uri = uri.withPath(Uri.Path("/v1/parties")),
JsArray(Vector.empty)
).flatMap {
case (status, output) =>
status shouldBe StatusCodes.BadRequest
assertStatus(output, StatusCodes.BadRequest)
val errorMsg = expectedOneErrorMessage(output)
errorMsg should include("Cannot read JSON: <[]>")
errorMsg should include("must be a list with at least 1 element")
}: Future[Assertion]
}

View File

@ -45,7 +45,7 @@ object HttpServiceTestFixture {
dars: List[File],
jdbcConfig: Option[JdbcConfig],
staticContentConfig: Option[StaticContentConfig]
)(testFn: (Uri, DomainJsonEncoder, DomainJsonDecoder) => Future[A])(
)(testFn: (Uri, DomainJsonEncoder, DomainJsonDecoder, LedgerClient) => Future[A])(
implicit asys: ActorSystem,
mat: Materializer,
aesf: ExecutionSequencerFactory,
@ -92,8 +92,9 @@ object HttpServiceTestFixture {
val fa: Future[A] = for {
(_, httpPort) <- httpServiceF
(encoder, decoder) <- codecsF
client <- clientF
uri = Uri.from(scheme = "http", host = "localhost", port = httpPort)
a <- testFn(uri, encoder, decoder)
a <- testFn(uri, encoder, decoder, client)
} yield a
fa.onComplete { _ =>