From bee1d5266577112aaaff33fe03ae48212de7ab32 Mon Sep 17 00:00:00 2001 From: Ravi Patel <82118011+rpatel-figure@users.noreply.github.com> Date: Thu, 16 Feb 2023 17:14:14 -0600 Subject: [PATCH] Update metadata-asset-model to v1, polish the README badges (#62) * Add build badge to README, tidy up existing badges * Update metadata-asset-model to v1.0.0 --- README.md | 8 ++- .../io/provenance/scope/loan/LoanScope.kt | 10 ++- .../loan/contracts/RecordLoanContract.kt | 15 +++- .../RecordLoanValidationRequestContract.kt | 14 ++-- .../RecordLoanValidationResultsContract.kt | 26 +++---- .../loan/utility/ContractRequirements.kt | 34 ++++++++-- .../loan/utility/DataValidationExtensions.kt | 26 +++++-- .../loan/utility/LoanContractValidations.kt | 57 ++++++++++++---- .../RecordLoanValidationRequestUnitTest.kt | 2 +- .../RecordLoanValidationResultsUnitTest.kt | 10 +-- .../scope/loan/test/Constructors.kt | 2 +- .../scope/loan/test/KotestHelpers.kt | 68 +++++++------------ .../scope/loan/test/TestDataGenerators.kt | 4 +- .../utility/ContractRequirementsUnitTest.kt | 50 +++++++++++--- .../loan/utility/DataValidationUnitTest.kt | 12 ++-- gradle/libs.versions.toml | 4 +- 16 files changed, 215 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 83c7262..817d4b0 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,19 @@ Defines a loan package scope specification and [p8e](https://github.com/provenance-io/p8e-scope-sdk/) smart contracts that can be executed against it. ## Status +[![Build][build-badge]][build-workflow] [![stability-beta][stability-badge]][stability-info] [![Code Coverage][code-coverage-badge]][code-coverage-report] -[![Latest Release][release-badge]][release-latest] - [![LOC][loc-badge]][loc-url] ### Artifacts +[![Latest Release][release-badge]][release-latest] #### Contracts JAR [![Contracts Artifact][contracts-publication-badge]][contracts-publication-url] #### Protobuf JAR [![Proto Artifact][proto-publication-badge]][proto-publication-url] +[build-badge]: https://img.shields.io/github/actions/workflow/status/provenance-io/loan-package-contracts/build.yml?branch=main&style=for-the-badge +[build-workflow]: https://github.com/provenance-io/loan-package-contracts/actions/workflows/build.yml [stability-badge]: https://img.shields.io/badge/stability-beta-33bbff.svg?style=for-the-badge [stability-info]: https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta [code-coverage-badge]: https://img.shields.io/codecov/c/gh/provenance-io/loan-package-contracts/main?label=Codecov&style=for-the-badge @@ -25,7 +27,7 @@ smart contracts that can be executed against it. [proto-publication-url]: https://maven-badges.herokuapp.com/maven-central/io.provenance.loan-package/proto [license-badge]: https://img.shields.io/github/license/provenance-io/loan-package-contracts.svg [license-url]: https://github.com/provenance-io/loan-package-contracts/blob/main/LICENSE -[loc-badge]: https://tokei.rs/b1/github/provenance-io/loan-package-contracts +[loc-badge]: https://img.shields.io/tokei/lines/github/provenance-io/loan-package-contracts?style=for-the-badge [loc-url]: https://github.com/provenance-io/loan-package-contracts ## Development ### Commands diff --git a/contract/src/main/kotlin/io/provenance/scope/loan/LoanScope.kt b/contract/src/main/kotlin/io/provenance/scope/loan/LoanScope.kt index d352f5a..f4eb336 100644 --- a/contract/src/main/kotlin/io/provenance/scope/loan/LoanScope.kt +++ b/contract/src/main/kotlin/io/provenance/scope/loan/LoanScope.kt @@ -9,7 +9,7 @@ import tech.figure.asset.v1beta1.Asset import tech.figure.loan.v1beta1.LoanDocuments import tech.figure.servicing.v1beta1.LoanStateOuterClass.ServicingData import tech.figure.servicing.v1beta1.ServicingRightsOuterClass.ServicingRights -import tech.figure.validation.v1beta1.LoanValidation +import tech.figure.validation.v1beta2.LoanValidation /** * Denotes the string literals used in [Record] annotations for the [LoanScopeSpecification] and its contracts. @@ -19,7 +19,13 @@ object LoanScopeFacts { const val documents = "documents" const val servicingRights = "servicingRights" const val servicingData = "servicingData" + @Deprecated( + message = "This label is for a record no longer supported by the latest validation contracts.", + replaceWith = ReplaceWith("LoanScopeFacts.loanValidationMetadata", "io.provenance.scope.loan.LoanScopeFacts"), + level = DeprecationLevel.WARNING, + ) const val loanValidations = "loanValidations" + const val loanValidationMetadata = "loanValidation" const val eNote = "eNote" } @@ -69,7 +75,7 @@ data class LoanPackage( /** Servicing data for the loan, including a list of metadata on loan states. */ @Record(LoanScopeFacts.servicingData) var servicingData: ServicingData, /** A list of third-party validation iterations. */ - @Record(LoanScopeFacts.loanValidations) var loanValidations: LoanValidation, + @Record(LoanScopeFacts.loanValidationMetadata) var loanValidations: LoanValidation, /** The eNote for the loan. */ @Record(LoanScopeFacts.eNote) var eNote: ENote, ) diff --git a/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanContract.kt b/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanContract.kt index 16f24dd..56e5e33 100644 --- a/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanContract.kt +++ b/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanContract.kt @@ -32,8 +32,9 @@ import tech.figure.loan.v1beta1.LoanDocuments import tech.figure.loan.v1beta1.MISMOLoanMetadata import tech.figure.servicing.v1beta1.LoanStateOuterClass.ServicingData import tech.figure.servicing.v1beta1.ServicingRightsOuterClass.ServicingRights -import tech.figure.validation.v1beta1.LoanValidation import tech.figure.loan.v1beta1.Loan as FigureTechLoan +import tech.figure.validation.v1beta1.LoanValidation as LoanValidation_v1beta1 +import tech.figure.validation.v1beta2.LoanValidation as LoanValidation_v1beta2 @Participants(roles = [PartyType.OWNER]) @ScopeSpecification(["tech.figure.loan"]) @@ -110,9 +111,19 @@ open class RecordLoanContract( ) } + @Suppress("deprecation") @Function(invokedBy = PartyType.OWNER) @Record(LoanScopeFacts.loanValidations) - open fun recordValidationData(@Input(LoanScopeFacts.loanValidations) loanValidations: LoanValidation) = loanValidations.also( + /** + * Overwrites the value of the deprecated validation record. + * + * This function exists to provide backwards compatibility for scopes which used a validation record before the newer one was introduced. + */ + open fun recordOldValidationData(@Input(LoanScopeFacts.loanValidations) loanValidations: LoanValidation_v1beta1) = loanValidations + + @Function(invokedBy = PartyType.OWNER) + @Record(LoanScopeFacts.loanValidationMetadata) + open fun recordValidationData(@Input(LoanScopeFacts.loanValidationMetadata) loanValidations: LoanValidation_v1beta2) = loanValidations.also( loanValidationInputValidation ) diff --git a/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationRequestContract.kt b/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationRequestContract.kt index b94b7ce..510e39b 100644 --- a/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationRequestContract.kt +++ b/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationRequestContract.kt @@ -10,24 +10,24 @@ import io.provenance.scope.contract.spec.P8eContract import io.provenance.scope.loan.LoanScopeFacts import io.provenance.scope.loan.LoanScopeInputs import io.provenance.scope.loan.utility.ContractRequirementType.VALID_INPUT -import io.provenance.scope.loan.utility.loanValidationRequestValidation import io.provenance.scope.loan.utility.orError +import io.provenance.scope.loan.utility.validateLoanValidationRequest import io.provenance.scope.loan.utility.validateRequirements -import tech.figure.validation.v1beta1.LoanValidation -import tech.figure.validation.v1beta1.ValidationIteration -import tech.figure.validation.v1beta1.ValidationRequest +import tech.figure.validation.v1beta2.LoanValidation +import tech.figure.validation.v1beta2.ValidationIteration +import tech.figure.validation.v1beta2.ValidationRequest @Participants(roles = [PartyType.OWNER]) // TODO: Add/Change to VALIDATOR? @ScopeSpecification(["tech.figure.loan"]) open class RecordLoanValidationRequestContract( - @Record(name = LoanScopeFacts.loanValidations, optional = true) val validationRecord: LoanValidation?, + @Record(name = LoanScopeFacts.loanValidationMetadata, optional = true) val validationRecord: LoanValidation?, ) : P8eContract() { @Function(invokedBy = PartyType.OWNER) // TODO: Add/Change to VALIDATOR? - @Record(LoanScopeFacts.loanValidations) + @Record(LoanScopeFacts.loanValidationMetadata) open fun recordLoanValidationRequest(@Input(LoanScopeInputs.validationRequest) submission: ValidationRequest): LoanValidation { validateRequirements(VALID_INPUT) { - loanValidationRequestValidation(submission) + validateLoanValidationRequest(submission) validationRecord?.let { existingValidationRecord -> requireThat( existingValidationRecord.iterationList.none { iteration -> diff --git a/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationResultsContract.kt b/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationResultsContract.kt index 34209a3..9ff7156 100644 --- a/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationResultsContract.kt +++ b/contract/src/main/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationResultsContract.kt @@ -12,43 +12,35 @@ import io.provenance.scope.loan.LoanScopeInputs import io.provenance.scope.loan.utility.ContractRequirementType import io.provenance.scope.loan.utility.isSet import io.provenance.scope.loan.utility.isValid -import io.provenance.scope.loan.utility.loanValidationResultsValidation import io.provenance.scope.loan.utility.orError +import io.provenance.scope.loan.utility.raiseError +import io.provenance.scope.loan.utility.validateLoanValidationResults import io.provenance.scope.loan.utility.validateRequirements -import tech.figure.validation.v1beta1.LoanValidation -import tech.figure.validation.v1beta1.ValidationResponse +import tech.figure.validation.v1beta2.LoanValidation +import tech.figure.validation.v1beta2.ValidationResponse @Participants(roles = [PartyType.VALIDATOR]) @ScopeSpecification(["tech.figure.loan"]) open class RecordLoanValidationResultsContract( - @Record(LoanScopeFacts.loanValidations) val validationRecord: LoanValidation, + @Record(LoanScopeFacts.loanValidationMetadata) val validationRecord: LoanValidation, ) : P8eContract() { @Function(invokedBy = PartyType.VALIDATOR) - @Record(LoanScopeFacts.loanValidations) + @Record(LoanScopeFacts.loanValidationMetadata) open fun recordLoanValidationResults( @Input(LoanScopeInputs.validationResponse) submission: ValidationResponse ): LoanValidation { validateRequirements(ContractRequirementType.LEGAL_SCOPE_STATE, validationRecord.isSet() orError "A validation iteration must already exist for results to be submitted", ) - validateRequirements(ContractRequirementType.VALID_INPUT) { + validateRequirements(ContractRequirementType.VALID_INPUT) { requireThat( submission.requestId.isValid() orError "Response must have valid ID", ) - loanValidationResultsValidation(submission.results) + validateLoanValidationResults(submission.results) validationRecord.iterationList.singleOrNull { iteration -> iteration.request.requestId == submission.requestId // For now, we won't support letting results arrive before the request - }.let { maybeIteration -> - requireThat( - if (maybeIteration === null) { - false orError "No single validation iteration with a matching request ID exists" - } else { - (submission.results.resultSetProvider == maybeIteration.request.validatorName) orError - "Result set provider does not match what was requested in this validation iteration" - } - ) - } + } ?: raiseError("No single validation iteration with a matching request ID exists") } return validationRecord.iterationList.indexOfLast { iteration -> // The enforcement above ensures exactly one match iteration.request.requestId == submission.requestId diff --git a/contract/src/main/kotlin/io/provenance/scope/loan/utility/ContractRequirements.kt b/contract/src/main/kotlin/io/provenance/scope/loan/utility/ContractRequirements.kt index efbfb38..b3f877e 100644 --- a/contract/src/main/kotlin/io/provenance/scope/loan/utility/ContractRequirements.kt +++ b/contract/src/main/kotlin/io/provenance/scope/loan/utility/ContractRequirements.kt @@ -61,8 +61,23 @@ internal enum class ContractRequirementType( internal infix fun Boolean.orError(error: ContractViolation): ContractEnforcement = Pair(this, error) +/** + * Immediately raises a [ContractViolation]. + * + * @param error The violation message to propagate. + */ internal fun ContractEnforcementContext.raiseError(error: ContractViolation) = requireThat(false to error) +/** + * Defines a [ContractViolation] without immediately enforcing it. + * + * Should be used over [ContractEnforcementContext.raiseError] within [ContractEnforcementContext.requireThatEach]'s `requirement` body to prevent + * duplication in a resulting exception message. + * + * @param error The violation message to propagate if the violation is enforced. + */ +internal fun askToRaiseError(error: ContractViolation) = askThat(false to error) + /** * Performs validation of one or more [ContractEnforcement]s. */ @@ -128,6 +143,14 @@ private fun ContractViolationMap.handleViolations( throw requirementType.toException(overallViolationCount, formattedViolations) } +/** + * Creates a list of [ContractEnforcement]s without immediately enforcing them. + * + * Should be used over [ContractEnforcementContext.requireThat] within [ContractEnforcementContext.requireThatEach]'s `requirement` body to prevent + * duplication in a resulting exception message. + */ +internal fun askThat(vararg enforcements: ContractEnforcement): List = enforcements.toList() + /** * Defines a body in which [ContractEnforcement]s can be freely defined and then collectively evaluated. */ @@ -164,11 +187,12 @@ internal class ContractEnforcementContext( } } }.forEach { (violationMessage, iterations) -> - val iterationsLimit = 5 - iterations.joinToString( - limit = iterationsLimit, - truncated = "...(${(iterations.size - iterationsLimit)} more omitted)", - ).let { iterationIndicesSnippet -> + 5.let { iterationsLimit -> + iterations.joinToString( + limit = iterationsLimit, + truncated = "...(${(iterations.size - iterationsLimit)} more omitted)", + ) + }.let { iterationIndicesSnippet -> addViolation("$violationMessage [$iterationsDescription $iterationIndicesSnippet]") } } diff --git a/contract/src/main/kotlin/io/provenance/scope/loan/utility/DataValidationExtensions.kt b/contract/src/main/kotlin/io/provenance/scope/loan/utility/DataValidationExtensions.kt index 98d2162..8062c01 100644 --- a/contract/src/main/kotlin/io/provenance/scope/loan/utility/DataValidationExtensions.kt +++ b/contract/src/main/kotlin/io/provenance/scope/loan/utility/DataValidationExtensions.kt @@ -49,21 +49,37 @@ internal fun FigureTechDate?.isValidForSignedDate() = isSet() && this!!.value.is internal fun FigureTechUUID?.isValid() = isSet() && this!!.value.isNotBlank() && tryOrFalse { JavaUUID.fromString(value) } -internal fun ContractEnforcementContext.checksumValidation(parentDescription: String = "Input", checksum: FigureTechChecksum?) { +// TODO: Figure out how to DRY this method so it can be used with and without a ContractEnforcementContext, while still letting requireThatEach work +internal fun ContractEnforcementContext.validateChecksum( + parentDescription: String = "Input", + checksum: FigureTechChecksum? +): List = checksum.takeIf { it.isSet() }?.let { setChecksum -> requireThat( setChecksum.checksum.isNotBlank() orError "$parentDescription must have a valid checksum string", setChecksum.algorithm.isNotBlank() orError "$parentDescription must specify a checksum algorithm", ) } ?: raiseError("$parentDescription's checksum is not set") -} -internal fun ContractEnforcementContext.moneyValidation(parentDescription: String = "Input's money", money: FigureTechMoney?) { +internal fun checksumValidation( + parentDescription: String = "Input", + checksum: FigureTechChecksum? +): List = + checksum.takeIf { it.isSet() }?.let { setChecksum -> + askThat( + setChecksum.checksum.isNotBlank() orError "$parentDescription must have a valid checksum string", + setChecksum.algorithm.isNotBlank() orError "$parentDescription must specify a checksum algorithm", + ) + } ?: askToRaiseError("$parentDescription's checksum is not set") + +internal fun ContractEnforcementContext.moneyValidation( + parentDescription: String = "Input's money", + money: FigureTechMoney? +): List = money.takeIf { it.isSet() }?.let { setMoney -> requireThat( - setMoney.value.matches(Regex("^[-]?([0-9]+(?:[\\\\.][0-9]+)?|\\\\.[0-9]+)\$")) orError "$parentDescription must have a valid value", + setMoney.value.matches(Regex("^-?([0-9]+(?:[\\\\.][0-9]+)?|\\\\.[0-9]+)\$")) orError "$parentDescription must have a valid value", (setMoney.currency.length == 3 && setMoney.currency.all { character -> character.isLetter() }) orError "$parentDescription must have a 3-letter ISO 4217 currency", ) } ?: raiseError("$parentDescription is not set") -} diff --git a/contract/src/main/kotlin/io/provenance/scope/loan/utility/LoanContractValidations.kt b/contract/src/main/kotlin/io/provenance/scope/loan/utility/LoanContractValidations.kt index 0a8ea41..82f1426 100644 --- a/contract/src/main/kotlin/io/provenance/scope/loan/utility/LoanContractValidations.kt +++ b/contract/src/main/kotlin/io/provenance/scope/loan/utility/LoanContractValidations.kt @@ -5,9 +5,9 @@ import tech.figure.loan.v1beta1.LoanDocuments import tech.figure.servicing.v1beta1.LoanStateOuterClass.LoanStateMetadata import tech.figure.servicing.v1beta1.ServicingRightsOuterClass.ServicingRights import tech.figure.util.v1beta1.DocumentMetadata -import tech.figure.validation.v1beta1.LoanValidation -import tech.figure.validation.v1beta1.ValidationRequest -import tech.figure.validation.v1beta1.ValidationResults +import tech.figure.validation.v1beta2.LoanValidation +import tech.figure.validation.v1beta2.ValidationRequest +import tech.figure.validation.v1beta2.ValidationResultsMetadata import io.dartinc.registry.v1beta1.Controller as ENoteController /** @@ -58,7 +58,7 @@ internal val documentValidation: ContractEnforcementContext.(DocumentMetadata) - setDocument.documentType.isNotBlank() orError "Document$documentIdSnippet is missing document type", setDocument.fileName.isNotBlank() orError "Document$documentIdSnippet is missing file name", ) - checksumValidation("Document$documentIdSnippet", setDocument.checksum) + validateChecksum("Document$documentIdSnippet", setDocument.checksum) } ?: raiseError("Document is not set") } @@ -80,7 +80,7 @@ internal val eNoteDocumentValidation: ContractEnforcementContext.(DocumentMetada setENote.documentType.isNotBlank() orError "eNote is missing document type", setENote.fileName.isNotBlank() orError "eNote is missing file name", ) - checksumValidation("eNote", setENote.checksum) + validateChecksum("eNote", setENote.checksum) } ?: raiseError("eNote document is not set") } @@ -141,7 +141,7 @@ internal val loanStateValidation: ContractEnforcementContext.(LoanStateMetadata) loanState.effectiveTime.isValidForLoanState() orError "Loan state$idSnippet must have valid effective time", loanState.uri.isNotBlank() orError "Loan state$idSnippet is missing URI", ) - checksumValidation("Loan state$idSnippet", loanState.checksum) + validateChecksum("Loan state$idSnippet", loanState.checksum) } internal val loanValidationInputValidation: (LoanValidation) -> Unit = { validationRecord -> @@ -166,20 +166,47 @@ internal val loanValidationInputValidation: (LoanValidation) -> Unit = { validat } } -internal val loanValidationResultsValidation: ContractEnforcementContext.(ValidationResults) -> List = { results -> +internal val loanValidationResultsValidation: (ValidationResultsMetadata) -> List = { results -> results.takeIf { it.isSet() }?.let { setResults -> - requireThat( - setResults.resultSetUuid.isValid() orError "Results must have valid result set UUID", - setResults.resultSetEffectiveTime.isValidAndNotInFuture() orError "Results must have valid effective time", - (setResults.validationExceptionCount >= 0) orError "Results report an invalid validation exception count", - (setResults.validationWarningCount >= 0) orError "Results report an invalid validation warning count", - (setResults.validationItemsCount > 0) orError "Results must have at least one validation item", - setResults.resultSetProvider.isNotBlank() orError "Results missing provider name", + listOf( + askThat( + setResults.id.isValid() orError "Results must have valid ID", + setResults.effectiveTime.isValidAndNotInFuture() orError "Results must have valid effective time", + setResults.uri.isNotBlank() orError "Results are missing a URI", + ), + checksumValidation("Results", setResults.checksum), + ).flatten() + } ?: askToRaiseError("Results are not set") +} + +internal val loanValidationRequestValidation: (ValidationRequest) -> List = { request -> + request.takeIf { it.isSet() }?.let { setRequest -> + askThat( + setRequest.requestId.isValid() orError "Request must have valid ID", + setRequest.effectiveTime.isValidAndNotInFuture() orError "Request must have valid effective time", + (setRequest.blockHeight >= 0L) orError "Request must have valid block height", + setRequest.validatorName.isNotBlank() orError "Request is missing validator name", + setRequest.requesterName.isNotBlank() orError "Request is missing requester name", ) + } ?: askToRaiseError("Request is not set") +} + +// TODO: Figure out how to DRY this method so it can be used with and without a ContractEnforcementContext, while still letting requireThatEach work +internal val validateLoanValidationResults: ContractEnforcementContext.(ValidationResultsMetadata) -> List = { results -> + results.takeIf { it.isSet() }?.let { setResults -> + listOf( + requireThat( + setResults.id.isValid() orError "Results must have valid ID", + setResults.effectiveTime.isValidAndNotInFuture() orError "Results must have valid effective time", + setResults.uri.isNotBlank() orError "Results are missing a URI", + ), + validateChecksum("Results", setResults.checksum), + ).flatten() } ?: raiseError("Results are not set") } -internal val loanValidationRequestValidation: ContractEnforcementContext.(ValidationRequest) -> List = { request -> +// TODO: Figure out how to DRY this method so it can be used with and without a ContractEnforcementContext, while still letting requireThatEach work +internal val validateLoanValidationRequest: ContractEnforcementContext.(ValidationRequest) -> List = { request -> request.takeIf { it.isSet() }?.let { setRequest -> requireThat( setRequest.requestId.isValid() orError "Request must have valid ID", diff --git a/contract/src/test/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationRequestUnitTest.kt b/contract/src/test/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationRequestUnitTest.kt index 6100e01..5b2b2ac 100644 --- a/contract/src/test/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationRequestUnitTest.kt +++ b/contract/src/test/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationRequestUnitTest.kt @@ -13,7 +13,7 @@ import io.provenance.scope.loan.test.MetadataAssetModelArbs.anyInvalidUuid import io.provenance.scope.loan.test.MetadataAssetModelArbs.anyValidValidationRequest import io.provenance.scope.loan.test.shouldHaveViolationCount import io.provenance.scope.loan.utility.ContractViolationException -import tech.figure.validation.v1beta1.ValidationRequest +import tech.figure.validation.v1beta2.ValidationRequest class RecordLoanValidationRequestUnitTest : WordSpec({ "recordLoanValidationRequest" When { diff --git a/contract/src/test/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationResultsUnitTest.kt b/contract/src/test/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationResultsUnitTest.kt index 6d473a1..35312a7 100644 --- a/contract/src/test/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationResultsUnitTest.kt +++ b/contract/src/test/kotlin/io/provenance/scope/loan/contracts/RecordLoanValidationResultsUnitTest.kt @@ -18,8 +18,8 @@ import io.provenance.scope.loan.test.breakOffLast import io.provenance.scope.loan.test.shouldHaveViolationCount import io.provenance.scope.loan.utility.ContractViolationException import io.provenance.scope.loan.utility.IllegalContractStateException -import tech.figure.validation.v1beta1.LoanValidation -import tech.figure.validation.v1beta1.ValidationResponse +import tech.figure.validation.v1beta2.LoanValidation +import tech.figure.validation.v1beta2.ValidationResponse class RecordLoanValidationResultsUnitTest : WordSpec({ "recordLoanValidationResults" When { @@ -54,7 +54,7 @@ class RecordLoanValidationResultsUnitTest : WordSpec({ } } } - "given an input without a valid result set ID" should { + "given an input without a valid result ID" should { "throw an appropriate exception" { checkAll( anyValidValidationRecord, @@ -76,13 +76,13 @@ class RecordLoanValidationResultsUnitTest : WordSpec({ submission = ValidationResponse.newBuilder().also { responseBuilder -> responseBuilder.requestId = randomNewIteration.request.requestId responseBuilder.results = randomNewIteration.results.toBuilder().also { resultsBuilder -> - resultsBuilder.resultSetUuid = randomInvalidId + resultsBuilder.id = randomInvalidId }.build() }.build() ) }.let { exception -> exception shouldHaveViolationCount 1 - exception.message shouldContain "Results must have valid result set UUID" + exception.message shouldContain "Results must have valid ID" } } } diff --git a/contract/src/test/kotlin/io/provenance/scope/loan/test/Constructors.kt b/contract/src/test/kotlin/io/provenance/scope/loan/test/Constructors.kt index 91ccf09..ea37f42 100644 --- a/contract/src/test/kotlin/io/provenance/scope/loan/test/Constructors.kt +++ b/contract/src/test/kotlin/io/provenance/scope/loan/test/Constructors.kt @@ -8,7 +8,7 @@ import io.provenance.scope.loan.contracts.RecordLoanValidationResultsContract import tech.figure.asset.v1beta1.Asset import tech.figure.servicing.v1beta1.LoanStateOuterClass.ServicingData import tech.figure.servicing.v1beta1.ServicingRightsOuterClass.ServicingRights -import tech.figure.validation.v1beta1.LoanValidation +import tech.figure.validation.v1beta2.LoanValidation import java.util.UUID as JavaUUID import tech.figure.util.v1beta1.UUID as FigureTechUUID diff --git a/contract/src/test/kotlin/io/provenance/scope/loan/test/KotestHelpers.kt b/contract/src/test/kotlin/io/provenance/scope/loan/test/KotestHelpers.kt index 92f2b24..ecfa1d8 100644 --- a/contract/src/test/kotlin/io/provenance/scope/loan/test/KotestHelpers.kt +++ b/contract/src/test/kotlin/io/provenance/scope/loan/test/KotestHelpers.kt @@ -49,13 +49,13 @@ import tech.figure.servicing.v1beta1.LoanStateOuterClass.ServicingData import tech.figure.servicing.v1beta1.ServicingRightsOuterClass.ServicingRights import tech.figure.util.v1beta1.AssetType import tech.figure.util.v1beta1.DocumentMetadata -import tech.figure.validation.v1beta1.LoanValidation -import tech.figure.validation.v1beta1.ValidationItem -import tech.figure.validation.v1beta1.ValidationIteration -import tech.figure.validation.v1beta1.ValidationOutcome -import tech.figure.validation.v1beta1.ValidationRequest -import tech.figure.validation.v1beta1.ValidationResponse -import tech.figure.validation.v1beta1.ValidationResults +import tech.figure.validation.v1beta2.LoanValidation +import tech.figure.validation.v1beta2.ValidationItem +import tech.figure.validation.v1beta2.ValidationIteration +import tech.figure.validation.v1beta2.ValidationOutcome +import tech.figure.validation.v1beta2.ValidationRequest +import tech.figure.validation.v1beta2.ValidationResponse +import tech.figure.validation.v1beta2.ValidationResultsMetadata import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.time.Instant @@ -343,50 +343,32 @@ internal object MetadataAssetModelArbs { anyUuid, anyPastNonEpochTimestamp, PrimitiveArbs.anyNonEmptyString, - Arb.int(min = 0), - Arb.int(min = 0), - Arb.list(anyValidValidationItem, range = 1..(if (KotestConfig.runTestsExtended) 50 else 10)), - Arb.list(anyValidDocumentMetadata, range = 0..(if (KotestConfig.runTestsExtended) 50 else 10)), - ) { requestId, resultSetId, effectiveTime, providerName, exceptionCount, warningCount, validationItems, policyDocuments -> + anyValidChecksum, + ) { requestId, resultsId, effectiveTime, uri, checksum -> ValidationResponse.newBuilder().also { responseBuilder -> responseBuilder.requestId = requestId - responseBuilder.results = ValidationResults.newBuilder().also { resultsBuilder -> - resultsBuilder.resultSetUuid = resultSetId - resultsBuilder.resultSetEffectiveTime = effectiveTime - resultsBuilder.resultSetProvider = providerName - resultsBuilder.validationExceptionCount = exceptionCount - resultsBuilder.validationWarningCount = warningCount - resultsBuilder.clearValidationItems() - resultsBuilder.addAllValidationItems(validationItems) - resultsBuilder.clearPolicyDocuments() - resultsBuilder.addAllPolicyDocuments(policyDocuments) + responseBuilder.results = ValidationResultsMetadata.newBuilder().also { resultsBuilder -> + resultsBuilder.id = resultsId + resultsBuilder.uri = uri + resultsBuilder.checksum = checksum + resultsBuilder.effectiveTime = effectiveTime }.build() }.build() } - fun anyValidValidationIteration( - itemCount: IntRange = 1..5, - policyDocumentCount: IntRange = 0..5, - ): Arb = Arb.bind( + fun anyValidValidationIteration(): Arb = Arb.bind( anyValidValidationRequest, anyUuid, anyPastNonEpochTimestamp, - Arb.int(min = 0), - Arb.int(min = 0), - Arb.list(anyValidValidationItem, itemCount), - Arb.list(anyValidDocumentMetadata, policyDocumentCount), - ) { request, resultSetId, effectiveTime, exceptionCount, warningCount, validationItems, policyDocuments -> + PrimitiveArbs.anyNonEmptyString, + anyValidChecksum, + ) { request, resultsId, effectiveTime, uri, checksum -> ValidationIteration.newBuilder().also { iterationBuilder -> iterationBuilder.request = request - iterationBuilder.results = ValidationResults.newBuilder().also { resultsBuilder -> - resultsBuilder.resultSetUuid = resultSetId - resultsBuilder.resultSetEffectiveTime = effectiveTime - resultsBuilder.resultSetProvider = request.validatorName - resultsBuilder.validationExceptionCount = exceptionCount - resultsBuilder.validationWarningCount = warningCount - resultsBuilder.clearValidationItems() - resultsBuilder.addAllValidationItems(validationItems) - resultsBuilder.clearPolicyDocuments() - resultsBuilder.addAllPolicyDocuments(policyDocuments) + iterationBuilder.results = ValidationResultsMetadata.newBuilder().also { resultsBuilder -> + resultsBuilder.id = resultsId + resultsBuilder.uri = uri + resultsBuilder.checksum = checksum + resultsBuilder.effectiveTime = effectiveTime }.build() }.build() } @@ -482,11 +464,9 @@ internal object MetadataAssetModelArbs { fun anyValidValidationRecord( iterationCount: Int, slippage: Int = 30, - itemCounts: IntRange = 1..5, - policyDocumentCounts: IntRange = 0..5, ): Arb = Arb.bind( Arb.list( - anyValidValidationIteration(itemCount = itemCounts, policyDocumentCount = policyDocumentCounts), + anyValidValidationIteration(), range = iterationCount..iterationCount ), anyUuidSet(size = iterationCount, slippage = slippage), diff --git a/contract/src/test/kotlin/io/provenance/scope/loan/test/TestDataGenerators.kt b/contract/src/test/kotlin/io/provenance/scope/loan/test/TestDataGenerators.kt index edf4023..ffa6c43 100644 --- a/contract/src/test/kotlin/io/provenance/scope/loan/test/TestDataGenerators.kt +++ b/contract/src/test/kotlin/io/provenance/scope/loan/test/TestDataGenerators.kt @@ -37,7 +37,7 @@ internal class TestDataGenerators : WordSpec({ LoanScopeFacts.eNote to randomLoanPackage.eNote, LoanScopeFacts.servicingRights to randomLoanPackage.servicingRights, LoanScopeFacts.servicingData to randomLoanPackage.servicingData, - LoanScopeFacts.loanValidations to randomLoanPackage.loanValidations, + LoanScopeFacts.loanValidationMetadata to randomLoanPackage.loanValidations, LoanScopeFacts.documents to randomLoanPackage.documents, ) ) @@ -53,7 +53,7 @@ internal class TestDataGenerators : WordSpec({ LoanScopeFacts.eNote to randomLoanPackage.eNote, LoanScopeFacts.servicingRights to randomLoanPackage.servicingRights, LoanScopeFacts.servicingData to randomLoanPackage.servicingData, - LoanScopeFacts.loanValidations to randomLoanPackage.loanValidations, + LoanScopeFacts.loanValidationMetadata to randomLoanPackage.loanValidations, LoanScopeFacts.documents to randomLoanPackage.documents, ) ) diff --git a/contract/src/test/kotlin/io/provenance/scope/loan/utility/ContractRequirementsUnitTest.kt b/contract/src/test/kotlin/io/provenance/scope/loan/utility/ContractRequirementsUnitTest.kt index e2e6bfc..b7b7483 100644 --- a/contract/src/test/kotlin/io/provenance/scope/loan/utility/ContractRequirementsUnitTest.kt +++ b/contract/src/test/kotlin/io/provenance/scope/loan/utility/ContractRequirementsUnitTest.kt @@ -11,15 +11,20 @@ import io.kotest.matchers.ints.shouldBeInRange import io.kotest.matchers.ints.shouldBeLessThan import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContainIgnoringCase +import io.kotest.matchers.string.shouldMatch import io.kotest.matchers.types.shouldBeTypeOf import io.kotest.property.Arb import io.kotest.property.arbitrary.flatMap import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.intArray import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.of +import io.kotest.property.arbitrary.pair import io.kotest.property.checkAll import io.provenance.scope.loan.test.MetadataAssetModelArbs.anyValidDocumentMetadata import io.provenance.scope.loan.test.MetadataAssetModelArbs.anyValidServicingData -import io.provenance.scope.loan.test.PrimitiveArbs +import io.provenance.scope.loan.test.PrimitiveArbs.anyContractEnforcement import io.provenance.scope.loan.test.shouldHaveViolationCount class ContractRequirementsUnitTest : WordSpec({ @@ -50,7 +55,7 @@ class ContractRequirementsUnitTest : WordSpec({ } } "return state violations only for failed conditions" { - checkAll(Arb.list(PrimitiveArbs.anyContractEnforcement)) { enforcementList -> + checkAll(Arb.list(anyContractEnforcement)) { enforcementList -> val expectedOverallViolationCount = getExpectedViolationCount(enforcementList) if (expectedOverallViolationCount > 0U) { shouldThrow { @@ -66,7 +71,7 @@ class ContractRequirementsUnitTest : WordSpec({ } } "return input violations only for failed conditions" { - checkAll(Arb.list(PrimitiveArbs.anyContractEnforcement)) { enforcementList -> + checkAll(Arb.list(anyContractEnforcement)) { enforcementList -> val expectedOverallViolationCount = getExpectedViolationCount(enforcementList) if (expectedOverallViolationCount > 0U) { shouldThrow { @@ -83,15 +88,40 @@ class ContractRequirementsUnitTest : WordSpec({ } } "invoked with a function body" should { - "!properly handle calling requireThatEach on a collection with violations in multiple iterations" { - shouldThrow { - // TODO: Implement + "properly handle calling requireThatEach on a collection with violations in multiple iterations" { + checkAll( + Arb.int(2..30).flatMap { itemCount -> + Arb.pair( + Arb.intArray(Arb.of(itemCount), Arb.int(min = 0)), + Arb.int(1..itemCount) + ) + }.map { (items, invalidItemCount) -> + Pair( + items.toList(), + items.take(invalidItemCount) + ) + }, + ) { (items, invalidItems) -> + shouldThrow { + validateRequirements(ContractRequirementType.VALID_INPUT) { + items.requireThatEach { item -> + askThat( + (item !in invalidItems) orError "Item is invalid" + ) + } + } + }.let { exception -> + exception shouldHaveViolationCount 1 + exception.message shouldMatch Regex( + ".*Item is invalid \\[Iterations (\\d+, )*\\d+(\\, \\.\\.\\.\\(\\d+ more omitted\\))?\\].*" + ) + } } } "return state violations only for failed conditions" { checkAll( - Arb.list(PrimitiveArbs.anyContractEnforcement), - Arb.list(PrimitiveArbs.anyContractEnforcement), + Arb.list(anyContractEnforcement), + Arb.list(anyContractEnforcement), ) { enforcementListA, enforcementListB -> val expectedOverallViolationCount = getExpectedViolationCount(enforcementListA + enforcementListB) if (expectedOverallViolationCount > 0U) { @@ -121,8 +151,8 @@ class ContractRequirementsUnitTest : WordSpec({ } "return input violations only for failed conditions" { checkAll( - Arb.list(PrimitiveArbs.anyContractEnforcement), - Arb.list(PrimitiveArbs.anyContractEnforcement), + Arb.list(anyContractEnforcement), + Arb.list(anyContractEnforcement), ) { enforcementListA, enforcementListB -> val expectedOverallViolationCount = getExpectedViolationCount(enforcementListA + enforcementListB) if (expectedOverallViolationCount > 0U) { diff --git a/contract/src/test/kotlin/io/provenance/scope/loan/utility/DataValidationUnitTest.kt b/contract/src/test/kotlin/io/provenance/scope/loan/utility/DataValidationUnitTest.kt index d27b471..b1cda1a 100644 --- a/contract/src/test/kotlin/io/provenance/scope/loan/utility/DataValidationUnitTest.kt +++ b/contract/src/test/kotlin/io/provenance/scope/loan/utility/DataValidationUnitTest.kt @@ -26,8 +26,8 @@ import io.provenance.scope.loan.test.PrimitiveArbs.anyNonUuidString import io.provenance.scope.loan.test.PrimitiveArbs.anyZoneOffset import io.provenance.scope.loan.test.shouldHaveViolationCount import io.provenance.scope.util.toProtoTimestamp -import tech.figure.validation.v1beta1.ValidationIteration -import tech.figure.validation.v1beta1.ValidationRequest +import tech.figure.validation.v1beta2.ValidationIteration +import tech.figure.validation.v1beta2.ValidationRequest import java.time.LocalDateTime import java.time.ZoneOffset import tech.figure.util.v1beta1.Checksum as FigureTechChecksum @@ -188,7 +188,7 @@ class DataValidationUnitTest : WordSpec({ "throw an appropriate exception for a default instance" { shouldThrow { validateRequirements(ContractRequirementType.VALID_INPUT) { - checksumValidation(checksum = FigureTechChecksum.getDefaultInstance()) + validateChecksum(checksum = FigureTechChecksum.getDefaultInstance()) } }.let { exception -> exception.message shouldContain "Input's checksum is not set" @@ -198,7 +198,7 @@ class DataValidationUnitTest : WordSpec({ checkAll(anyNonEmptyString) { randomChecksum -> shouldThrow { validateRequirements(ContractRequirementType.VALID_INPUT) { - checksumValidation( + validateChecksum( checksum = FigureTechChecksum.newBuilder().apply { clearAlgorithm() checksum = randomChecksum @@ -214,7 +214,7 @@ class DataValidationUnitTest : WordSpec({ checkAll(anyNonEmptyString) { randomAlgorithm -> shouldThrow { validateRequirements(ContractRequirementType.VALID_INPUT) { - checksumValidation( + validateChecksum( checksum = FigureTechChecksum.newBuilder().apply { clearChecksum() algorithm = randomAlgorithm @@ -229,7 +229,7 @@ class DataValidationUnitTest : WordSpec({ "not throw an exception for any non-empty checksum and algorithm strings" { checkAll(anyNonEmptyString, anyNonEmptyString) { randomChecksum, randomAlgorithm -> validateRequirements(ContractRequirementType.VALID_INPUT) { - checksumValidation( + validateChecksum( checksum = FigureTechChecksum.newBuilder().apply { checksum = randomChecksum algorithm = randomAlgorithm diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4e760aa..8d9156b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ ## Kotlin kotlin = "1.8.10" ## Provenance -metadataAssetModel = "0.1.14" -p8eScopeSdk = "0.6.2" +metadataAssetModel = "1.0.0" +p8eScopeSdk = "0.6.4" ## Protocol Buffers grpc = "1.45.0" krotoPlus = "0.6.1"