From e5e897ce7a8a5474267a4a9794505b33401962c7 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Mon, 3 Aug 2026 21:11:05 +0800 Subject: [PATCH] fix: make issuance request transitions atomic --- .../domain/model/IssuanceRequest.java | 21 ++-- .../domain/model/IssuanceRequestTest.java | 100 ++++++++++++++++++ 2 files changed, 114 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequest.java b/src/main/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequest.java index 43b357b..b786650 100644 --- a/src/main/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequest.java +++ b/src/main/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequest.java @@ -49,27 +49,33 @@ public class IssuanceRequest { public void prepareMessage(String requestTimestamp, String messageText) { requireStatus(IssuanceRequestStatus.DRAFT); - this.requestTimestamp = Objects.requireNonNull(requestTimestamp, "requestTimestamp must not be null"); - this.messageText = Objects.requireNonNull(messageText, "messageText must not be null"); + String validatedRequestTimestamp = Objects.requireNonNull(requestTimestamp, "requestTimestamp must not be null"); + String validatedMessageText = Objects.requireNonNull(messageText, "messageText must not be null"); + this.requestTimestamp = validatedRequestTimestamp; + this.messageText = validatedMessageText; this.status = IssuanceRequestStatus.MESSAGE_PREPARED; } public void recordDigest(String digest) { requireStatus(IssuanceRequestStatus.MESSAGE_PREPARED); - this.digest = Objects.requireNonNull(digest, "digest must not be null"); + String validatedDigest = Objects.requireNonNull(digest, "digest must not be null"); + this.digest = validatedDigest; this.status = IssuanceRequestStatus.DIGESTED; } public void recordSignature(String signingKeyRef, String signature) { requireStatus(IssuanceRequestStatus.DIGESTED); - this.signingKeyRef = Objects.requireNonNull(signingKeyRef, "signingKeyRef must not be null"); - this.signature = Objects.requireNonNull(signature, "signature must not be null"); + String validatedSigningKeyRef = Objects.requireNonNull(signingKeyRef, "signingKeyRef must not be null"); + String validatedSignature = Objects.requireNonNull(signature, "signature must not be null"); + this.signingKeyRef = validatedSigningKeyRef; + this.signature = validatedSignature; this.status = IssuanceRequestStatus.SIGNED; } public void packagePayload(String payloadJson) { requireStatus(IssuanceRequestStatus.SIGNED); - this.payloadJson = Objects.requireNonNull(payloadJson, "payloadJson must not be null"); + String validatedPayloadJson = Objects.requireNonNull(payloadJson, "payloadJson must not be null"); + this.payloadJson = validatedPayloadJson; this.status = IssuanceRequestStatus.PACKAGED; } @@ -151,8 +157,9 @@ public class IssuanceRequest { if (denominationTotal.compareTo(normalizedAmount) != 0) { throw new IllegalArgumentException("denomination total must equal totalAmount"); } + String validatedCurrency = Objects.requireNonNull(currency, "currency must not be null"); this.totalAmount = normalizedAmount; - this.currency = Objects.requireNonNull(currency, "currency must not be null"); + this.currency = validatedCurrency; this.denominations = Collections.unmodifiableList(copiedDenominations); } diff --git a/src/test/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequestTest.java b/src/test/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequestTest.java index 7275fc3..822ceff 100644 --- a/src/test/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequestTest.java +++ b/src/test/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequestTest.java @@ -4,7 +4,9 @@ import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -96,12 +98,52 @@ class IssuanceRequestTest { .isInstanceOf(IllegalStateException.class); } + @Test + void rejectedDraftUpdateLeavesEveryObservableFieldUnchanged() { + IssuanceRequest request = draftRequest(); + RequestSnapshot original = RequestSnapshot.from(request); + + assertThatThrownBy(() -> request.updateDraft( + new BigDecimal("200.00"), null, Arrays.asList(new DenominationItem(100, 2)))) + .isInstanceOf(NullPointerException.class); + + original.assertMatches(request); + } + + @Test + void rejectedMessagePreparationLeavesEveryObservableFieldUnchanged() { + IssuanceRequest request = draftRequest(); + RequestSnapshot original = RequestSnapshot.from(request); + + assertThatThrownBy(() -> request.prepareMessage("20260803120000", null)) + .isInstanceOf(NullPointerException.class); + + original.assertMatches(request); + } + + @Test + void rejectedSignatureRecordingLeavesEveryObservableFieldUnchanged() { + IssuanceRequest request = draftRequest(); + request.prepareMessage("20260803120000", "ISSUE|..."); + request.recordDigest("0123456789ABCDEF"); + RequestSnapshot original = RequestSnapshot.from(request); + + assertThatThrownBy(() -> request.recordSignature("sm2-key-02", null)) + .isInstanceOf(NullPointerException.class); + + original.assertMatches(request); + } + private IssuanceRequest preparedSignedAndPackagedRequest() { IssuanceRequest request = draftRequest(); request.prepareMessage("20260803120000", "ISSUE|BKCHCNBJ00001|ORG_3A4B5C6D7E8F|50000.00|100:400,50:200|DC|20260803120000"); + assertThat(request.getStatus()).isEqualTo(IssuanceRequestStatus.MESSAGE_PREPARED); request.recordDigest("0123456789ABCDEF"); + assertThat(request.getStatus()).isEqualTo(IssuanceRequestStatus.DIGESTED); request.recordSignature("sm2-key-02", "signature"); + assertThat(request.getStatus()).isEqualTo(IssuanceRequestStatus.SIGNED); request.packagePayload("{\"requestId\":\"b7f572a0-1e8d-4e3c-8e42-44b5994f0429\"}"); + assertThat(request.getStatus()).isEqualTo(IssuanceRequestStatus.PACKAGED); return request; } @@ -114,4 +156,62 @@ class IssuanceRequestTest { "DC", Arrays.asList(new DenominationItem(100, 400), new DenominationItem(50, 200))); } + + private static final class RequestSnapshot { + private final IssuanceApplicationId id; + private final String bankCode; + private final String organizationId; + private final BigDecimal totalAmount; + private final String currency; + private final List denominations; + private final String requestTimestamp; + private final String messageText; + private final String digest; + private final String signature; + private final String signingKeyRef; + private final String payloadJson; + private final IssuanceRequestStatus status; + private final CentralBankReceiveStatus centralBankReceiveStatus; + private final Instant centralBankReceivedAt; + + private RequestSnapshot(IssuanceRequest request) { + this.id = request.getId(); + this.bankCode = request.getBankCode(); + this.organizationId = request.getOrganizationId(); + this.totalAmount = request.getTotalAmount(); + this.currency = request.getCurrency(); + this.denominations = new ArrayList(request.getDenominations()); + this.requestTimestamp = request.getRequestTimestamp(); + this.messageText = request.getMessageText(); + this.digest = request.getDigest(); + this.signature = request.getSignature(); + this.signingKeyRef = request.getSigningKeyRef(); + this.payloadJson = request.getPayloadJson(); + this.status = request.getStatus(); + this.centralBankReceiveStatus = request.getCentralBankReceiveStatus(); + this.centralBankReceivedAt = request.getCentralBankReceivedAt(); + } + + static RequestSnapshot from(IssuanceRequest request) { + return new RequestSnapshot(request); + } + + void assertMatches(IssuanceRequest request) { + assertThat(request.getId()).isEqualTo(id); + assertThat(request.getBankCode()).isEqualTo(bankCode); + assertThat(request.getOrganizationId()).isEqualTo(organizationId); + assertThat(request.getTotalAmount()).isEqualByComparingTo(totalAmount); + assertThat(request.getCurrency()).isEqualTo(currency); + assertThat(request.getDenominations()).containsExactlyElementsOf(denominations); + assertThat(request.getRequestTimestamp()).isEqualTo(requestTimestamp); + assertThat(request.getMessageText()).isEqualTo(messageText); + assertThat(request.getDigest()).isEqualTo(digest); + assertThat(request.getSignature()).isEqualTo(signature); + assertThat(request.getSigningKeyRef()).isEqualTo(signingKeyRef); + assertThat(request.getPayloadJson()).isEqualTo(payloadJson); + assertThat(request.getStatus()).isEqualTo(status); + assertThat(request.getCentralBankReceiveStatus()).isEqualTo(centralBankReceiveStatus); + assertThat(request.getCentralBankReceivedAt()).isEqualTo(centralBankReceivedAt); + } + } }