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() .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 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() .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.read(repeatedDigest, "$.data.digest")) .isEqualTo(com.jayway.jsonpath.JsonPath.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() .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() .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 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 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; } } }