You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
digital-rmb-backend/src/test/java/com/yau/digitalrmb/institutionidentity/TransactionInformationIdent...

950 lines
63 KiB
Java

package com.yau.digitalrmb.institutionidentity;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationRequest;
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationVerification;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.ControlSystemSignatureEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.ControlSystemSignatureMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.QuotaControlBitEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.QuotaControlBitMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyBatchMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierOperationLogEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierOperationLogMapper;
import com.yau.digitalrmb.security.application.JwtTokenService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class TransactionInformationIdentifierControllerTest {
private static final String BASE_PATH =
"/api/v1/institution-identifiers/transaction-information-identifiers";
private static final String QUOTA_BASE_PATH =
"/api/v1/institution-identifiers/quota-requests";
private static final String CENTRAL_QUOTA_REQUEST_BASE_PATH =
"/api/v1/institution-identifiers/central-bank/quota-requests";
private static final String VERIFICATION_BASE_PATH =
"/api/v1/institution-identifiers/quota-verifications";
private static final String CONTROL_SIGNATURE_BASE_PATH =
"/api/v1/institution-identifiers/control-system-signatures";
private static final String COMMERCIAL_BANK_CONTROL_SIGNATURE_BASE_PATH =
"/api/v1/institution-identifiers/commercial-bank/control-system-signatures";
private static final String QUOTA_CONTROL_BASE_PATH =
"/api/v1/institution-identifiers/quota-control-bits";
private static final String CENTRAL_QUOTA_CONTROL_BASE_PATH =
"/api/v1/institution-identifiers/central-bank/quota-control-bits";
private static final String STANDARD_CURRENCY_BASE_PATH =
"/api/v1/institution-identifiers/standard-currencies";
private static final String USER_ID = "00000000-0000-0000-0000-000000009004";
private static final long SCHOOL_ID = 1001L;
private static final long CLASS_ID = 2001L;
@Autowired private MockMvc mvc;
@Autowired private JwtTokenService jwtTokenService;
@Autowired private JdbcTemplate jdbcTemplate;
@Autowired private InstitutionIdentifierApplicationMapper applicationMapper;
@Autowired private CurrencyGenerationRequestMapper requestMapper;
@Autowired private CurrencyGenerationVerificationMapper verificationMapper;
@Autowired private TransactionInformationIdentifierMapper transactionMapper;
@Autowired private TransactionInformationIdentifierOperationLogMapper transactionLogMapper;
@Autowired private ControlSystemSignatureMapper controlSignatureMapper;
@Autowired private QuotaControlBitMapper quotaControlBitMapper;
@Autowired private StandardCurrencyBatchMapper standardCurrencyBatchMapper;
@Autowired private StandardCurrencyMapper standardCurrencyMapper;
@Autowired private StuModuleScoreDetailsMapper scoreMapper;
@Autowired private InstitutionKeyService keyService;
@Autowired private InstitutionIdentityCryptography cryptography;
private String authorization;
@BeforeEach
void setUp() {
jdbcTemplate.update("DELETE FROM standard_currency");
jdbcTemplate.update("DELETE FROM standard_currency_batch");
jdbcTemplate.update("DELETE FROM quota_control_bit");
jdbcTemplate.update("DELETE FROM control_system_signature");
jdbcTemplate.update("DELETE FROM transaction_information_identifier_operation_log");
jdbcTemplate.update("DELETE FROM transaction_information_identifier");
jdbcTemplate.update("DELETE FROM currency_generation_verification_operation_log");
jdbcTemplate.update("DELETE FROM currency_generation_verification");
jdbcTemplate.update("DELETE FROM currency_generation_request_operation_log");
jdbcTemplate.update("DELETE FROM currency_generation_request");
jdbcTemplate.update("DELETE FROM institution_identifier_training_error");
jdbcTemplate.update("DELETE FROM institution_identifier_operation_log");
jdbcTemplate.update("DELETE FROM institution_identifier_application");
jdbcTemplate.update("DELETE FROM institution_sm2_key_audit");
jdbcTemplate.update("DELETE FROM institution_sm2_key");
jdbcTemplate.update("DELETE FROM stu_module_score_details");
authorization = "Bearer " + jwtTokenService.issueFor(USER_ID, SCHOOL_ID, CLASS_ID,
"module-four-tester", Collections.singleton("STUDENT")).accessToken();
}
@Test
void rejectsDigestWhenStepThreeHasNotReturnedResponseAndPrepareRouteIsRemoved() throws Exception {
mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.andExpect(status().isNotFound());
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤三尚未完成确认报文返回")));
assertThat(transactionMapper.selectList(null)).isEmpty();
}
@Test
void recordsEveryConcatenationErrorAndReturnsFixedMessage() throws Exception {
insertCompletedPrecedingData();
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85[ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("数据拼接错误"));
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"999.00|ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("数据拼接错误"));
assertThat(transactionMapper.selectList(null)).isEmpty();
StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID)
.eq(StuModuleScoreDetailsEntity::getSerialNumber, 3));
assertThat(score).isNotNull();
assertThat(score.getCompletionStatus()).isEqualTo("2");
}
@Test
void flowsCompletedPreviousDataAndPersistsEveryModuleFourStep() throws Exception {
PrecedingData preceding = insertCompletedPrecedingData();
String originalText = "1589.85|ORG_3A4B5C6D7E8F|20260730153330";
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + originalText + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest")
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.originalText").doesNotExist())
.andExpect(jsonPath("$.data.transactionTimestamp").doesNotExist())
.andExpect(jsonPath("$.data.digestAlgorithm").doesNotExist());
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.transactionIdentifier")
.value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.digest").doesNotExist());
mvc.perform(get(BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("ASSEMBLED"))
.andExpect(jsonPath("$.data.transactionIdentifier")
.value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}")));
List<TransactionInformationIdentifierEntity> records = transactionMapper.selectList(null);
assertThat(records).hasSize(1);
TransactionInformationIdentifierEntity persisted = records.get(0);
assertThat(persisted.getVerificationId()).isEqualTo(preceding.verificationId);
assertThat(persisted.getRequestId()).isEqualTo(preceding.requestId);
assertThat(persisted.getIdentifierApplicationId()).isEqualTo(preceding.applicationId);
assertThat(persisted.getAmount()).isEqualByComparingTo("1589.85");
assertThat(persisted.getInstitutionIdentifier()).isEqualTo("ORG_3A4B5C6D7E8F");
assertThat(persisted.getOriginalText()).isEqualTo(originalText);
assertThat(persisted.getTransactionTimestamp()).isEqualTo("20260730153330");
assertThat(persisted.getDigest()).isEqualTo(cryptography.sm3(originalText));
assertThat(persisted.getTransactionIdentifier()).isEqualTo("TXN_" + persisted.getDigest());
assertThat(persisted.getStatus()).isEqualTo("ASSEMBLED");
assertThat(transactionLogMapper.selectList(null))
.extracting(TransactionInformationIdentifierOperationLogEntity::getOperation)
.containsExactly("DIGEST", "ASSEMBLE");
StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID)
.eq(StuModuleScoreDetailsEntity::getSerialNumber, 3));
assertThat(score).isNotNull();
assertThat(score.getSchedule()).isEqualTo(44.44);
assertThat(score.getScoreProject()).isEqualTo(44.44);
}
@Test
void flowsCompleteStepFiveFromSourceDataThroughCentralBankReceive() throws Exception {
insertCompletedPrecedingData();
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(get(QUOTA_BASE_PATH + "/source-data").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.transactionIdentifier")
.value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(3))
.andExpect(jsonPath("$.data.transactionIdentifierExists").doesNotExist())
.andExpect(jsonPath("$.data.conclusion").doesNotExist());
mvc.perform(post(QUOTA_BASE_PATH + "/steps/prepare").header("Authorization", authorization))
.andExpect(status().isNotFound());
String quotaOriginalText = "1589.85|ORG_3A4B5C6D7E8F|"
+ transactionMapper.selectList(null).get(0).getTransactionIdentifier();
mvc.perform(post(QUOTA_BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + quotaOriginalText + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").value(
org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.quotaRequestId").doesNotExist())
.andExpect(jsonPath("$.data.quotaOriginalText").doesNotExist());
InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID);
String bankPrivateKey = keyService.commercialBankKey(subject, "module-four-tester").getPrivateKey();
mvc.perform(post(QUOTA_BASE_PATH + "/steps/sign").header("Authorization", authorization)
.contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").value(
org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.quotaSigningKeyId").doesNotExist())
.andExpect(jsonPath("$.data.quotaSignature").doesNotExist());
long stepFiveRecordId = transactionMapper.selectList(null).get(0).getId();
mvc.perform(post(QUOTA_BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + quotaOriginalText + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
assertThat(transactionMapper.selectList(null)).hasSize(1);
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepFiveRecordId);
assertThat(transactionMapper.selectList(null).get(0).getQuotaSignature()).isNull();
mvc.perform(post(QUOTA_BASE_PATH + "/steps/sign").header("Authorization", authorization)
.contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\"}"))
.andExpect(status().isOk());
mvc.perform(post(QUOTA_BASE_PATH + "/steps/sign").header("Authorization", authorization)
.contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")));
assertThat(transactionMapper.selectList(null)).hasSize(1);
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepFiveRecordId);
mvc.perform(post(QUOTA_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(6))
.andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty())
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_")))
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.quotaRequestMessage").doesNotExist());
mvc.perform(get(CENTRAL_QUOTA_REQUEST_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(1))
.andExpect(jsonPath("$.data.length()").value(7));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/send").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(6))
.andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty())
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_")))
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.centralBankReceiveStatus").doesNotExist());
mvc.perform(get(QUOTA_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(6));
mvc.perform(get(CENTRAL_QUOTA_REQUEST_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(2))
.andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty())
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_")))
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.signature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(7))
.andExpect(jsonPath("$.data.requestMessage").doesNotExist());
mvc.perform(post(CENTRAL_QUOTA_REQUEST_BASE_PATH + "/steps/receive")
.header("Authorization", authorization))
.andExpect(status().isNotFound());
TransactionInformationIdentifierEntity persisted = transactionMapper.selectList(null).get(0);
assertThat(persisted.getStatus()).isEqualTo("QUOTA_RECEIVED");
assertThat(persisted.getQuotaDigest()).isEqualTo(cryptography.sm3(persisted.getQuotaOriginalText()));
assertThat(cryptography.verify(keyService.commercialBankKey(subject, "module-four-tester").getPublicKey(),
persisted.getQuotaDigest(), persisted.getQuotaSignature())).isTrue();
assertThat(persisted.getQuotaRequestMessage()).contains(persisted.getQuotaRequestId(),
persisted.getQuotaDigest(), persisted.getQuotaSignature());
assertThat(transactionLogMapper.selectList(null))
.extracting(TransactionInformationIdentifierOperationLogEntity::getOperation)
.containsExactly("DIGEST", "ASSEMBLE", "QUOTA_DIGEST", "QUOTA_SIGN",
"QUOTA_DIGEST", "QUOTA_SIGN", "QUOTA_SIGN",
"QUOTA_PACKAGE", "QUOTA_SEND");
}
@Test
void flowsStepSixDataThroughCentralBankSignatureAndCommercialBankReceive() throws Exception {
flowsCompleteStepFiveFromSourceDataThroughCentralBankReceive();
InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID);
String bankPublicKey = keyService.commercialBankKey(subject, "module-four-tester").getPublicKey();
String centralPrivateKey = keyService.centralBankKey(subject, "module-four-tester").getPrivateKey();
String verificationOriginalText = transactionMapper.selectList(null).get(0).getQuotaOriginalText();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/concatenate-original-text")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.originalText").value(verificationOriginalText))
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + verificationOriginalText + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.h1").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.h2").doesNotExist());
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest")
.header("Authorization", authorization).contentType("application/json")
.content("{\"publicKey\":\"WRONG_BANK_SECOND_PUBLIC_KEY\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest")
.header("Authorization", authorization).contentType("application/json")
.content("{\"publicKey\":\"" + bankPublicKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.h2").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.h1").doesNotExist());
long stepSixRecordId = transactionMapper.selectList(null).get(0).getId();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + verificationOriginalText + "\"}"))
.andExpect(status().isOk());
assertThat(transactionMapper.selectList(null)).hasSize(1);
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepSixRecordId);
assertThat(transactionMapper.selectList(null).get(0).getQuotaSignatureDigest()).isNull();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest")
.header("Authorization", authorization).contentType("application/json")
.content("{\"publicKey\":\"" + bankPublicKey + "\"}"))
.andExpect(status().isOk());
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest")
.header("Authorization", authorization).contentType("application/json")
.content("{\"publicKey\":\"" + bankPublicKey + "\"}"))
.andExpect(status().isOk());
assertThat(transactionMapper.selectList(null)).hasSize(1);
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepSixRecordId);
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/compare-digests")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.h1EqualsH2").value(true))
.andExpect(jsonPath("$.data.conclusion").value("一致,验签通过"));
TransactionInformationIdentifierEntity verified = transactionMapper.selectList(null).get(0);
assertThat(verified.getStatus()).isEqualTo("QUOTA_VERIFIED");
assertThat(verified.getQuotaRecomputedDigest()).isEqualTo(
cryptography.sm3(verified.getQuotaOriginalText()));
assertThat(verified.getQuotaSignatureDigest()).isEqualTo(verified.getQuotaDigest());
assertThat(verified.getQuotaSignatureValid()).isTrue();
assertThat(verified.getQuotaDigestMatches()).isTrue();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/concatenate-original-text")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.originalText").value(verificationOriginalText));
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + verificationOriginalText + "\"}"))
.andExpect(status().isOk());
assertThat(transactionMapper.selectList(null).get(0).getQuotaSignatureDigest()).isNull();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/recover-signature-digest")
.header("Authorization", authorization).contentType("application/json")
.content("{\"publicKey\":\"" + bankPublicKey + "\"}"))
.andExpect(status().isOk());
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/compare-digests")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.h1EqualsH2").value(true));
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/compare-digests")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.h1EqualsH2").value(true));
assertThat(transactionMapper.selectList(null)).hasSize(1);
assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepSixRecordId);
verified = transactionMapper.selectList(null).get(0);
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/prepare")
.header("Authorization", authorization))
.andExpect(status().isNotFound());
String controlOriginalText = verified.getAmount().setScale(2).toPlainString() + "|"
+ verified.getInstitutionIdentifier() + "|" + verified.getTransactionIdentifier();
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"999.00|"
+ verified.getInstitutionIdentifier() + "|" + verified.getTransactionIdentifier()
+ "\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("与当前用户前序数据不一致"),
org.hamcrest.Matchers.containsString("第2次错误"))));
assertThat(controlSignatureMapper.selectList(null)).isEmpty();
String firstDigest = mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + controlOriginalText + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.digest").value(cryptography.sm3(controlOriginalText)))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.originalText").doesNotExist())
.andExpect(jsonPath("$.data.controlId").doesNotExist())
.andReturn().getResponse().getContentAsString();
String repeatedDigest = mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/digest")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"" + controlOriginalText + "\"}"))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
assertThat(com.jayway.jsonpath.JsonPath.<String>read(repeatedDigest, "$.data.digest"))
.isEqualTo(com.jayway.jsonpath.JsonPath.<String>read(firstDigest, "$.data.digest"));
assertThat(controlSignatureMapper.selectList(null)).hasSize(1);
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"privateKey\":\"WRONG_CENTRAL_PRIVATE_KEY\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第3次错误")));
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/sign")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"privateKey\":\"" + centralPrivateKey + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.responseMessage").value(org.hamcrest.Matchers.containsString(
"\"cbCtrlSignature\"")));
mvc.perform(get(COMMERCIAL_BANK_CONTROL_SIGNATURE_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(1))
.andExpect(jsonPath("$.data.ctrlId").isNotEmpty())
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_")))
.andExpect(jsonPath("$.data.cbCtrlSignature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(6));
mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/send").header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SENT"));
mvc.perform(get(COMMERCIAL_BANK_CONTROL_SIGNATURE_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(2))
.andExpect(jsonPath("$.data.ctrlId").isNotEmpty())
.andExpect(jsonPath("$.data.amount").value(1589.85))
.andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F"))
.andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_")))
.andExpect(jsonPath("$.data.cbCtrlSignature").isNotEmpty())
.andExpect(jsonPath("$.data.length()").value(6));
ControlSystemSignatureEntity persisted = controlSignatureMapper.selectList(null).get(0);
TransactionInformationIdentifierEntity transaction = transactionMapper.selectList(null).get(0);
assertThat(persisted.getTransactionIdentifierId()).isEqualTo(transaction.getId());
assertThat(persisted.getOriginalText()).isEqualTo(
transaction.getAmount().setScale(2).toPlainString() + "|"
+ transaction.getInstitutionIdentifier() + "|" + transaction.getTransactionIdentifier());
assertThat(persisted.getDigest()).isEqualTo(cryptography.sm3(persisted.getOriginalText()));
assertThat(persisted.getControlSignature()).isEqualTo(persisted.getSignature());
assertThat(cryptography.verify(keyService.centralBankKey(
new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID), "module-four-tester").getPublicKey(),
persisted.getDigest(), persisted.getControlSignature())).isTrue();
assertThat(persisted.getResponseMessage()).contains(persisted.getControlId(), persisted.getControlSignature());
assertThat(persisted.getStatus()).isEqualTo("SENT");
StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID)
.eq(StuModuleScoreDetailsEntity::getSerialNumber, 3));
assertThat(score.getCompletionStatus()).isEqualTo("3");
}
@Test
void flowsUserInputThroughCompleteStepEightAndAccumulatesErrors() throws Exception {
flowsStepSixDataThroughCentralBankSignatureAndCommercialBankReceive();
InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID);
String bankPrivateKey = keyService.commercialBankKey(subject, "module-four-tester").getPrivateKey();
mvc.perform(get(CENTRAL_QUOTA_CONTROL_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(1))
.andExpect(jsonPath("$.data.quotaId").value(org.hamcrest.Matchers.nullValue()))
.andExpect(jsonPath("$.data.length()").value(6));
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/static")
.header("Authorization", authorization))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("当前没有步骤八额度控制位记录"));
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/dynamic")
.header("Authorization", authorization))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("当前没有步骤八额度控制位记录"));
String staticSegmentResponse = mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.staticSegment").value(
org.hamcrest.Matchers.matchesPattern("STATIC_[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andReturn().getResponse().getContentAsString();
String staticSegment = com.jayway.jsonpath.JsonPath.read(staticSegmentResponse, "$.data.staticSegment");
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/static")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.staticSegment").value(staticSegment))
.andExpect(jsonPath("$.data.length()").value(1));
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/dynamic")
.header("Authorization", authorization))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("当前尚未生成步骤八动态段"));
String firstDynamicSegmentResponse = mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/dynamic-segment")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.dynamicSegment").value(
org.hamcrest.Matchers.matchesPattern("DYN_[A-Z0-9]{8}_\\d{14}")))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andReturn().getResponse().getContentAsString();
String firstDynamicSegment = com.jayway.jsonpath.JsonPath.read(
firstDynamicSegmentResponse, "$.data.dynamicSegment");
mvc.perform(get(QUOTA_CONTROL_BASE_PATH + "/segments/dynamic")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.dynamicSegment").value(firstDynamicSegment))
.andExpect(jsonPath("$.data.length()").value(1));
long stepEightRecordId = quotaControlBitMapper.selectList(null).get(0).getId();
String secondDynamicSegmentResponse = mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/dynamic-segment")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.dynamicSegment").value(
org.hamcrest.Matchers.matchesPattern("DYN_[A-Z0-9]{8}_\\d{14}")))
.andReturn().getResponse().getContentAsString();
String secondDynamicSegment = com.jayway.jsonpath.JsonPath.read(
secondDynamicSegmentResponse, "$.data.dynamicSegment");
assertThat(secondDynamicSegment).isNotEqualTo(firstDynamicSegment);
assertThat(quotaControlBitMapper.selectList(null)).hasSize(1);
assertThat(quotaControlBitMapper.selectList(null).get(0).getId()).isEqualTo(stepEightRecordId);
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/read-central-bank-signature")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.centralBankSignatureSegment").value(
org.hamcrest.Matchers.startsWith("CB_CTRL_")))
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data.status").doesNotExist())
.andExpect(jsonPath("$.data.completeControlBit").doesNotExist());
QuotaControlBitEntity beforeSign = quotaControlBitMapper.selectList(null).get(0);
assertThat(beforeSign.getCompleteControlBit()).isNull();
String completeControlBit = beforeSign.getStaticSegment() + "|" + beforeSign.getDynamicSegment() + "|"
+ beforeSign.getCentralBankSignatureSegment();
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\""
+ completeControlBit + "\",\"hashAlgorithm\":\"SM3\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第4次错误")));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\""
+ completeControlBit + "_TAMPERED\",\"hashAlgorithm\":\"SM2\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.allOf(
org.hamcrest.Matchers.containsString("完整额度控制位与静态段、动态段及央行签名段拼接结果不一致"),
org.hamcrest.Matchers.containsString("第5次错误"))));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"WRONG_BANK_PRIVATE_KEY\",\"completeControlBit\":\""
+ completeControlBit + "\",\"hashAlgorithm\":\"SM2\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第6次错误")));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\""
+ completeControlBit + "\",\"hashAlgorithm\":\"SM2\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.signature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.andExpect(jsonPath("$.data.length()").value(1));
assertThat(quotaControlBitMapper.selectList(null).get(0).getCompleteControlBit())
.isEqualTo(completeControlBit);
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED"))
.andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.containsString("\"quotaId\"")));
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.staticSegment").value(
org.hamcrest.Matchers.matchesPattern("STATIC_[0-9A-F]{64}")));
QuotaControlBitEntity resetByStatic = quotaControlBitMapper.selectList(null).get(0);
assertThat(resetByStatic.getId()).isEqualTo(stepEightRecordId);
assertThat(resetByStatic.getStatus()).isEqualTo("STATIC_READY");
assertThat(resetByStatic.getDynamicSegment()).isNull();
assertThat(resetByStatic.getCentralBankSignatureSegment()).isNull();
assertThat(resetByStatic.getCompleteControlBit()).isNull();
assertThat(resetByStatic.getBankSignature()).isNull();
assertThat(resetByStatic.getRequestMessage()).isNull();
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/dynamic-segment")
.header("Authorization", authorization)).andExpect(status().isOk());
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/read-central-bank-signature")
.header("Authorization", authorization)).andExpect(status().isOk());
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/assemble")
.header("Authorization", authorization)).andExpect(status().isOk());
String regeneratedCompleteControlBit = quotaControlBitMapper.selectList(null).get(0).getCompleteControlBit();
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/sign")
.header("Authorization", authorization).contentType("application/json")
.content("{\"privateKey\":\"" + bankPrivateKey + "\",\"completeControlBit\":\""
+ regeneratedCompleteControlBit + "\",\"hashAlgorithm\":\"SM2\"}"))
.andExpect(status().isOk());
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/package").header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/send").header("Authorization", authorization))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RECEIVED"))
.andExpect(jsonPath("$.data.receiveStatus").value(2));
mvc.perform(get(CENTRAL_QUOTA_CONTROL_BASE_PATH).header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value(2))
.andExpect(jsonPath("$.data.quotaId").value(org.hamcrest.Matchers.startsWith("QC_")))
.andExpect(jsonPath("$.data.staticSegment").value(
org.hamcrest.Matchers.matchesPattern("STATIC_[0-9A-F]{64}")))
.andExpect(jsonPath("$.data.dynamicSegment").value(
org.hamcrest.Matchers.matchesPattern("DYN_[A-Z0-9]{8}_\\d{14}")))
.andExpect(jsonPath("$.data.cbSignatureSegment").value(
org.hamcrest.Matchers.startsWith("CB_CTRL_")))
.andExpect(jsonPath("$.data.bankSignature").value(
org.hamcrest.Matchers.matchesPattern("[0-9A-F]+")))
.andExpect(jsonPath("$.data.length()").value(6));
mvc.perform(post(CENTRAL_QUOTA_CONTROL_BASE_PATH + "/steps/receive")
.header("Authorization", authorization))
.andExpect(status().isNotFound());
QuotaControlBitEntity stored = quotaControlBitMapper.selectList(null).get(0);
assertThat(stored.getStatus()).isEqualTo("RECEIVED");
assertThat(stored.getReceiveStatus()).isEqualTo(2);
assertThat(stored.getRequestMessage()).contains(stored.getQuotaId(), stored.getStaticSegment(),
stored.getDynamicSegment(), stored.getCentralBankSignatureSegment(), stored.getBankSignature());
StuModuleScoreDetailsEntity score = scoreMapper.selectOne(new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID)
.eq(StuModuleScoreDetailsEntity::getSerialNumber, 3));
assertThat(score.getCompletionStatus()).isEqualTo("6");
}
@Test
void generatesStepNineStandardCurrenciesAndAccumulatesInputErrors() throws Exception {
flowsUserInputThroughCompleteStepEightAndAccumulatesErrors();
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/decompose")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("DECOMPOSED"))
.andExpect(jsonPath("$.data.totalCount").value(27))
.andExpect(jsonPath("$.data.breakdown[0].denomination").value(100.0))
.andExpect(jsonPath("$.data.breakdown[0].count").value(15))
.andExpect(jsonPath("$.data.breakdown[10].denomination").value(0.01))
.andExpect(jsonPath("$.data.breakdown[10].count").value(0));
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/confirm-rule")
.header("Authorization", authorization).contentType("application/json")
.content("{\"prefix\":\"RMB\",\"idRule\":\"批次号+枚序号+随机校验码\"," +
"\"currencyStatus\":\"待生效\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第7次错误")));
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/confirm-rule")
.header("Authorization", authorization).contentType("application/json")
.content("{\"prefix\":\"DC\",\"idRule\":\"批次号+枚序号+随机校验码\"," +
"\"currencyStatus\":\"待生效\"}"))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RULE_CONFIRMED"));
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/generate")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("GENERATED"))
.andExpect(jsonPath("$.data.currencies.length()").value(27))
.andExpect(jsonPath("$.data.currencies[0].sequenceNumber").value(1))
.andExpect(jsonPath("$.data.currencies[0].denomination").value(100.0))
.andExpect(jsonPath("$.data.currencies[0].currencyId").value(
org.hamcrest.Matchers.matchesPattern("DC_[0-9]{8}_[0-9]{3}_001_[0-9A-Z]{6}")))
.andExpect(jsonPath("$.data.currencies[0].completeCurrency").value(
org.hamcrest.Matchers.containsString("_ORG_3A4B5C6D7E8F_100.00_")))
.andExpect(jsonPath("$.data.currencies[26].denomination").value(0.05))
.andExpect(jsonPath("$.data.currencies[26].status").value("待生效"));
mvc.perform(get(STANDARD_CURRENCY_BASE_PATH + "/currencies")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(27))
.andExpect(jsonPath("$.data[0].sequenceNumber").value(1))
.andExpect(jsonPath("$.data[0].denomination").value(100.0))
.andExpect(jsonPath("$.data[0].currencyId").value(
org.hamcrest.Matchers.matchesPattern("DC_[0-9]{8}_[0-9]{3}_001_[0-9A-Z]{6}")))
.andExpect(jsonPath("$.data[0].completeCurrency").value(
org.hamcrest.Matchers.containsString("_ORG_3A4B5C6D7E8F_100.00_")))
.andExpect(jsonPath("$.data[0].status").value(1))
.andExpect(jsonPath("$.data[0].randomCheckCode").doesNotExist())
.andExpect(jsonPath("$.data[0].institutionIdentifier").doesNotExist());
List<StandardCurrencyEntity> currencies = standardCurrencyMapper.selectList(null);
assertThat(standardCurrencyBatchMapper.selectList(null)).hasSize(1);
assertThat(currencies).hasSize(27);
assertThat(currencies.stream().map(StandardCurrencyEntity::getDenomination)
.reduce(BigDecimal.ZERO, BigDecimal::add)).isEqualByComparingTo("1589.85");
assertThat(currencies.stream().map(StandardCurrencyEntity::getCurrencyId).distinct().count()).isEqualTo(27);
StandardCurrencyEntity activated = currencies.get(0);
activated.setStatus("可用");
standardCurrencyMapper.updateById(activated);
mvc.perform(get(STANDARD_CURRENCY_BASE_PATH + "/currencies")
.header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(27))
.andExpect(jsonPath("$.data[0].status").value(2))
.andExpect(jsonPath("$.data[1].status").value(1));
activated.setStatus("待生效");
standardCurrencyMapper.updateById(activated);
List<Long> currencyIds = currencies.stream().map(StandardCurrencyEntity::getId)
.collect(java.util.stream.Collectors.toList());
long batchId = standardCurrencyBatchMapper.selectList(null).get(0).getId();
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/generate").header("Authorization", authorization))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("GENERATED"))
.andExpect(jsonPath("$.data.currencies.length()").value(27));
assertThat(standardCurrencyBatchMapper.selectList(null)).hasSize(1);
assertThat(standardCurrencyBatchMapper.selectList(null).get(0).getId()).isEqualTo(batchId);
assertThat(standardCurrencyMapper.selectList(null)).hasSize(27);
assertThat(standardCurrencyMapper.selectList(null).stream().map(StandardCurrencyEntity::getId)
.collect(java.util.stream.Collectors.toList())).containsExactlyElementsOf(currencyIds);
}
@Test
void rejectsStepNineUntilStepEightIsReceived() throws Exception {
mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/decompose")
.header("Authorization", authorization))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤八尚未完成央行接收")));
assertThat(standardCurrencyBatchMapper.selectList(null)).isEmpty();
}
@Test
void rejectsStepEightUntilStepSevenIsReceived() throws Exception {
mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment")
.header("Authorization", authorization).contentType("application/json")
.content("{\"amount\":1589.85,\"institutionIdentifier\":\"ORG_3A4B5C6D7E8F\"," +
"\"transactionIdentifier\":\"TXN_ABC\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤七尚未完成商业银行接收")));
assertThat(quotaControlBitMapper.selectList(null)).isEmpty();
}
@Test
void rejectsIncorrectStepFiveOriginalTextAndRecordsError() throws Exception {
insertCompletedPrecedingData();
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(post(QUOTA_BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"999.00|ORG_3A4B5C6D7E8F|TXN_"
+ "7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("数据拼接错误"));
assertThat(transactionMapper.selectList(null).get(0).getStatus()).isEqualTo("ASSEMBLED");
}
@Test
void rejectsQuotaVerificationUntilStepFiveIsReceived() throws Exception {
insertCompletedPrecedingData();
mvc.perform(post(VERIFICATION_BASE_PATH + "/steps/hash")
.header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|TXN_"
+ "7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F\"}"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"当前没有额度请求记录")));
}
@Test
void rejectsQuotaRequestUntilModuleFourIsAssembled() throws Exception {
insertCompletedPrecedingData();
mvc.perform(get(QUOTA_BASE_PATH + "/source-data").header("Authorization", authorization))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"步骤四尚未完成交易信息标识")));
mvc.perform(post(QUOTA_BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|TXN_"
+ "7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F\"}"))
.andExpect(status().isBadRequest());
assertThat(transactionMapper.selectList(null)).isEmpty();
}
@Test
void repeatedCorrectDigestUpdatesExistingRecordWithoutAddingData() throws Exception {
insertCompletedPrecedingData();
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730153330\"}"))
.andExpect(status().isOk());
long id = transactionMapper.selectList(null).get(0).getId();
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
.andExpect(status().isOk());
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization)
.contentType("application/json")
.content("{\"originalText\":\"1589.85|ORG_3A4B5C6D7E8F|20260730154444\"}"))
.andExpect(status().isOk());
assertThat(transactionMapper.selectList(null)).hasSize(1);
TransactionInformationIdentifierEntity updated = transactionMapper.selectList(null).get(0);
assertThat(updated.getId()).isEqualTo(id);
assertThat(updated.getTransactionTimestamp()).isEqualTo("20260730154444");
assertThat(updated.getStatus()).isEqualTo("DIGESTED");
assertThat(updated.getTransactionIdentifier()).isNull();
}
private PrecedingData insertCompletedPrecedingData() {
LocalDateTime now = LocalDateTime.now();
InstitutionIdentifierApplicationEntity application = new InstitutionIdentifierApplicationEntity();
application.setBankCode("BKCHCNBJ40001");
application.setUserId(USER_ID);
application.setSchoolId(SCHOOL_ID);
application.setClassId(CLASS_ID);
application.setRequestTimestamp("20260805180000");
application.setOriginalText("BKCHCNBJ40001|20260805180000");
application.setDigest("3A4B5C6D7E8F0000000000000000000000000000000000000000000000000000");
application.setBankSignature("3044AABB");
application.setStatus("FEEDBACKED");
application.setVerificationKeyId(InstitutionKeyService.BANK_SECOND_KEY);
application.setSignatureValid(true);
application.setDigestMatches(true);
application.setSigningKeyId(InstitutionKeyService.CENTRAL_FIRST_KEY);
application.setInstitutionIdentifier("ORG_3A4B5C6D7E8F");
application.setCentralBankSignature("3044CCDD");
application.setTrainingRound(1);
application.setScoringCriteria(0);
application.setCreatedAt(now);
application.setUpdatedAt(now);
application.setCreatedBy("module-four-tester");
application.setUpdatedBy("module-four-tester");
application.setDeleted(false);
applicationMapper.insert(application);
CurrencyGenerationRequestEntity request = new CurrencyGenerationRequestEntity();
request.setIdentifierApplicationId(application.getId());
request.setInstitutionIdentifier(application.getInstitutionIdentifier());
request.setFullInstitutionIdentifier(application.getDigest());
request.setAmount(new BigDecimal("1589.85"));
request.setDeliveryNodeCode("SYS_DC_001");
request.setRequestTimestamp("20260805180100");
request.setRequestOriginalText("ORG_3A4B5C6D7E8F|1589.85|SYS_DC_001|20260805180100");
request.setRequestDigest("7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F");
request.setBankSignature("3044AABB");
request.setSigningKeyId(InstitutionKeyService.BANK_SECOND_KEY);
request.setRequestMessage("{request:true}");
request.setStatus(CurrencyGenerationRequest.Status.RECEIVED.name());
request.setUserId(USER_ID);
request.setSchoolId(SCHOOL_ID);
request.setClassId(CLASS_ID);
request.setCreatedAt(now);
request.setUpdatedAt(now);
request.setCreatedBy("module-four-tester");
request.setUpdatedBy("module-four-tester");
request.setDeleted(false);
requestMapper.insert(request);
CurrencyGenerationVerificationEntity verification = new CurrencyGenerationVerificationEntity();
verification.setRequestId(request.getId());
verification.setRequestMessage(request.getRequestMessage());
verification.setRequestOriginalText(request.getRequestOriginalText());
verification.setRequestDigest(request.getRequestDigest());
verification.setBankSignature(request.getBankSignature());
verification.setBankKeyId(request.getSigningKeyId());
verification.setAmount(request.getAmount());
verification.setSignatureValid(true);
verification.setRecomputedDigest(request.getRequestDigest());
verification.setDigestMatches(true);
verification.setTotalQuota(new BigDecimal("500000000.00"));
verification.setUsedQuota(new BigDecimal("495000000.00"));
verification.setRemainingQuota(new BigDecimal("5000000.00"));
verification.setQuotaSufficient(true);
verification.setCentralBankKeyId(InstitutionKeyService.CENTRAL_FIRST_KEY);
verification.setCentralBankSignature("3044CCDD");
verification.setConfirmationTime("20260805180200");
verification.setConfirmationMessage("{confirmed:true}");
verification.setStatus(CurrencyGenerationVerification.Status.RESPONSE_RETURNED.name());
verification.setUserId(USER_ID);
verification.setSchoolId(SCHOOL_ID);
verification.setClassId(CLASS_ID);
verification.setCreatedAt(now);
verification.setUpdatedAt(now);
verification.setCreatedBy("module-four-tester");
verification.setUpdatedBy("module-four-tester");
verification.setDeleted(false);
verificationMapper.insert(verification);
return new PrecedingData(application.getId(), request.getId(), verification.getId());
}
private static class PrecedingData {
private final long applicationId;
private final long requestId;
private final long verificationId;
private PrecedingData(long applicationId, long requestId, long verificationId) {
this.applicationId = applicationId;
this.requestId = requestId;
this.verificationId = verificationId;
}
}
}