diff --git a/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/api/validation/ErrorFactoriesSpec.scala b/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/api/validation/ErrorFactoriesSpec.scala index 2ba482d91820..26cd1a25d3ab 100644 --- a/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/api/validation/ErrorFactoriesSpec.scala +++ b/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/platform/server/api/validation/ErrorFactoriesSpec.scala @@ -4,21 +4,22 @@ package com.daml import com.daml.error.{ - DamlContextualizedErrorLogger, ContextualizedErrorLogger, + DamlContextualizedErrorLogger, ErrorCodesVersionSwitcher, } import com.daml.ledger.api.domain.LedgerId import com.daml.logging.{ContextualizedLogger, LoggingContext} import com.daml.platform.server.api.validation.ErrorFactories import com.daml.platform.server.api.validation.ErrorFactories._ -import com.google.rpc.Status +import com.google.rpc.{ErrorInfo, RequestInfo, ResourceInfo, RetryInfo, Status} import io.grpc.Status.Code import io.grpc.StatusRuntimeException import io.grpc.protobuf.StatusProto import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.wordspec.AnyWordSpec +import com.google.protobuf import scala.jdk.CollectionConverters._ @@ -30,6 +31,9 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope private implicit val contextualizedErrorLogger: ContextualizedErrorLogger = new DamlContextualizedErrorLogger(logger, loggingContext, Some(correlationId)) + private val DefaultTraceIdRequestInfo: ErrorDetails.RequestInfoDetail = + ErrorDetails.RequestInfoDetail("trace-id") + "ErrorFactories" should { "return the DuplicateCommandException" in { assertVersionedError(_.duplicateCommandException)( @@ -39,6 +43,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v2_code = Code.ALREADY_EXISTS, v2_message = s"DUPLICATE_COMMAND(10,$correlationId): A command with the given command id has already been successfully processed", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("DUPLICATE_COMMAND"), + DefaultTraceIdRequestInfo, + ), ) } @@ -50,6 +58,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v2_code = Code.PERMISSION_DENIED, v2_message = s"An error occurred. Please contact the operator and inquire about the request $correlationId", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("PERMISSION_DENIED"), + DefaultTraceIdRequestInfo, + ), ) } @@ -68,6 +80,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v2_code = Code.NOT_FOUND, v2_message = s"LEDGER_CONFIGURATION_NOT_FOUND(11,$correlationId): The ledger configuration is not available.", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("LEDGER_CONFIGURATION_NOT_FOUND"), + DefaultTraceIdRequestInfo, + ), ) } } @@ -105,6 +121,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v2_code = Code.INVALID_ARGUMENT, v2_message = s"INVALID_FIELD(8,$correlationId): The submitted command has a field with invalid value: Invalid field my field: my message", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("INVALID_FIELD"), + DefaultTraceIdRequestInfo, + ), ) } } @@ -117,6 +137,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v2_code = Code.UNAUTHENTICATED, v2_message = s"An error occurred. Please contact the operator and inquire about the request $correlationId", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("UNAUTHENTICATED"), + DefaultTraceIdRequestInfo, + ), ) } @@ -137,6 +161,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v2_code = Code.NOT_FOUND, v2_message = s"LEDGER_ID_MISMATCH(11,$correlationId): Ledger ID 'received' not found. Actual Ledger ID is 'expected'.", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("LEDGER_ID_MISMATCH"), + DefaultTraceIdRequestInfo, + ), ) } } @@ -156,6 +184,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v1_details = Seq.empty, v2_code = Code.OUT_OF_RANGE, v2_message = s"PARTICIPANT_PRUNED_DATA_ACCESSED(12,$correlationId): my message", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("PARTICIPANT_PRUNED_DATA_ACCESSED"), + DefaultTraceIdRequestInfo, + ), ) } @@ -166,6 +198,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v1_details = Seq.empty, v2_code = Code.OUT_OF_RANGE, v2_message = s"REQUESTED_OFFSET_OUT_OF_RANGE(12,$correlationId): my message", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("REQUESTED_OFFSET_OUT_OF_RANGE"), + DefaultTraceIdRequestInfo, + ), ) } @@ -183,6 +219,11 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v1_details = expectedDetails, v2_code = Code.UNAVAILABLE, v2_message = s"SERVICE_NOT_RUNNING(1,$correlationId): Service has been shut down.", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("SERVICE_NOT_RUNNING"), + DefaultTraceIdRequestInfo, + ErrorDetails.RetryInfoDetail(1), + ), ) } } @@ -195,6 +236,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v2_code = Code.NOT_FOUND, v2_message = s"LEDGER_CONFIGURATION_NOT_FOUND(11,$correlationId): The ledger configuration is not available.", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("LEDGER_CONFIGURATION_NOT_FOUND"), + DefaultTraceIdRequestInfo, + ), ) } @@ -213,6 +258,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v2_code = Code.INVALID_ARGUMENT, v2_message = s"MISSING_FIELD(8,$correlationId): The submitted command is missing a mandatory field: my field", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("MISSING_FIELD"), + DefaultTraceIdRequestInfo, + ), ) } } @@ -232,6 +281,10 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope v2_code = Code.INVALID_ARGUMENT, v2_message = s"INVALID_ARGUMENT(8,$correlationId): The submitted command has invalid arguments: my message", + v2_details = Seq[ErrorDetails.ErrorDetail]( + ErrorDetails.ErrorInfoDetail("INVALID_ARGUMENT"), + DefaultTraceIdRequestInfo, + ), ) } } @@ -245,29 +298,76 @@ class ErrorFactoriesSpec extends AnyWordSpec with Matchers with TableDrivenPrope private def assertVersionedError( error: ErrorFactories => StatusRuntimeException - )(v1_code: Code, v1_message: String, v1_details: Seq[Any], v2_code: Code, v2_message: String) = { + )( + v1_code: Code, + v1_message: String, + v1_details: Seq[Any], + v2_code: Code, + v2_message: String, + v2_details: Seq[ErrorDetails.ErrorDetail], + ): Unit = { val errorFactoriesV1 = ErrorFactories(new ErrorCodesVersionSwitcher(false)) val errorFactoriesV2 = ErrorFactories(new ErrorCodesVersionSwitcher(true)) assertV1Error(error(errorFactoriesV1))(v1_code, v1_message, v1_details) - assertV2Error(error(errorFactoriesV2))(v2_code, v2_message) + assertV2Error(error(errorFactoriesV2))(v2_code, v2_message, v2_details) } private def assertV1Error( statusRuntimeException: StatusRuntimeException - )(expectedCode: Code, expectedMessage: String, expectedDetails: Seq[Any]) = { + )(expectedCode: Code, expectedMessage: String, expectedDetails: Seq[Any]): Unit = { val status = StatusProto.fromThrowable(statusRuntimeException) status.getCode shouldBe expectedCode.value() status.getMessage shouldBe expectedMessage - status.getDetailsList.asScala shouldBe expectedDetails + val _ = status.getDetailsList.asScala shouldBe expectedDetails } private def assertV2Error( statusRuntimeException: StatusRuntimeException - )(expectedCode: Code, expectedMessage: String) = { + )( + expectedCode: Code, + expectedMessage: String, + expectedDetails: Seq[ErrorDetails.ErrorDetail], + ): Unit = { val status = StatusProto.fromThrowable(statusRuntimeException) status.getCode shouldBe expectedCode.value() status.getMessage shouldBe expectedMessage - // TODO error codes: Assert error details + val details = status.getDetailsList.asScala.toSeq + val _ = ErrorDetails.from(details) should contain theSameElementsAs (expectedDetails) // TODO error codes: Assert logging } } + +object ErrorDetails { + + sealed trait ErrorDetail + + final case class ResourceInfoDetail(name: String, typ: String) extends ErrorDetail + + final case class ErrorInfoDetail(reason: String) extends ErrorDetail + + final case class RetryInfoDetail(retryDelayInSeconds: Long) extends ErrorDetail + + final case class RequestInfoDetail(requestId: String) extends ErrorDetail + + def from(anys: Seq[protobuf.Any]): Seq[ErrorDetail] = { + anys.toList.map(from) + } + + private def from(any: protobuf.Any): ErrorDetail = { + if (any.is(classOf[ResourceInfo])) { + val v = any.unpack(classOf[ResourceInfo]) + ResourceInfoDetail(v.getResourceType, v.getResourceName) + } else if (any.is(classOf[ErrorInfo])) { + val v = any.unpack(classOf[ErrorInfo]) + ErrorInfoDetail(v.getReason) + } else if (any.is(classOf[RetryInfo])) { + val v = any.unpack(classOf[RetryInfo]) + RetryInfoDetail(v.getRetryDelay.getSeconds) + } else if (any.is(classOf[RequestInfo])) { + val v = any.unpack(classOf[RequestInfo]) + RequestInfoDetail(v.getRequestId) + } else { + throw new IllegalStateException(s"Could not unpack value of: |$any|") + } + } +}