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.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.InstitutionIdentifierApplicationEntity; import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper; import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity; import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper; 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 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 CurrencyGenerationVerificationControllerTest { private static final String BASE_PATH = "/api/v1/institution-identifiers/currency-request-verifications"; private static final String USER_ID = "00000000-0000-0000-0000-000000009003"; 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 StuModuleScoreDetailsMapper scoreMapper; @Autowired private InstitutionIdentityCryptography cryptography; @Autowired private InstitutionKeyService keyService; private String authorization; private String bankPublicKey; private String centralPrivateKey; private long requestId; @BeforeEach void setUp() { 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_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, "step-three-tester", Collections.singleton("STUDENT")).accessToken(); InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, SCHOOL_ID, CLASS_ID); bankPublicKey = keyService.commercialBankKey(subject, "step-three-tester").getPublicKey(); centralPrivateKey = keyService.centralBankKey(subject, "step-three-tester").getPrivateKey(); requestId = insertReceivedRequest(subject); } @Test void everyStepThreeButtonHasIndependentEndpointAndState() throws Exception { String received = mvc.perform(post(BASE_PATH + "/steps/receive") .header("Authorization", authorization) .contentType("application/json") .content("{\"requestId\":" + requestId + "}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value("RECEIVED")) .andReturn().getResponse().getContentAsString(); long verificationId = number(received, "$.data.verificationId"); String repeated = mvc.perform(post(BASE_PATH + "/steps/receive") .header("Authorization", authorization) .contentType("application/json") .content("{\"requestId\":" + requestId + "}")) .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); assertThat(number(repeated, "$.data.verificationId")).isEqualTo(verificationId); assertThat(verificationMapper.selectList(null)).hasSize(1); mvc.perform(post(BASE_PATH + "/steps/verify-signature").header("Authorization", authorization) .contentType("application/json").content(step(verificationId, "publicKey", bankPublicKey))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SIGNATURE_VERIFIED")); String userInput = "用户输入的请求原文"; mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization) .contentType("application/json").content(digestStep(verificationId, userInput))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_CALCULATED")) .andExpect(jsonPath("$.data.recomputedDigest").value(cryptography.sm3(userInput))) .andExpect(jsonPath("$.data.digestMatches").value(org.hamcrest.Matchers.nullValue())); mvc.perform(post(BASE_PATH + "/steps/receive").header("Authorization", authorization) .contentType("application/json").content("{\"requestId\":" + requestId + "}")) .andExpect(status().isOk()).andExpect(jsonPath("$.data.verificationId").value(verificationId)) .andExpect(jsonPath("$.data.status").value("RECEIVED")) .andExpect(jsonPath("$.data.signatureValid").value(org.hamcrest.Matchers.nullValue())) .andExpect(jsonPath("$.data.recomputedDigest").value(org.hamcrest.Matchers.nullValue())); mvc.perform(post(BASE_PATH + "/steps/verify-signature").header("Authorization", authorization) .contentType("application/json").content(step(verificationId, "publicKey", bankPublicKey))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SIGNATURE_VERIFIED")); mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization) .contentType("application/json").content(digestStep(verificationId, "ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100"))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_CALCULATED")); mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization) .contentType("application/json").content(step(verificationId))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGEST_MATCHED")) .andExpect(jsonPath("$.data.digestMatches").value(true)); mvc.perform(post(BASE_PATH + "/steps/verify-quota").header("Authorization", authorization) .contentType("application/json").content(step(verificationId))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.totalQuota").value(50000.00)) .andExpect(jsonPath("$.data.usedQuota").value(0)) .andExpect(jsonPath("$.data.remainingQuota").value(50000.00)) .andExpect(jsonPath("$.data.currentAmount").value(1589.85)) .andExpect(jsonPath("$.data.quotaSufficient").value(true)) .andExpect(jsonPath("$.data.verificationId").doesNotExist()) .andExpect(jsonPath("$.data.status").doesNotExist()); mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization) .contentType("application/json").content(digestStep(verificationId, "ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100"))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value("QUOTA_VERIFIED")); mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization) .contentType("application/json").content(step(verificationId))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value("QUOTA_VERIFIED")) .andExpect(jsonPath("$.data.digestMatches").value(true)); mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization) .contentType("application/json").content(step(verificationId, "privateKey", centralPrivateKey))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("CONFIRMED")) .andExpect(jsonPath("$.data.centralBankSignature").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]+"))) .andExpect(jsonPath("$.data.confirmationMessage").value(org.hamcrest.Matchers.nullValue())); mvc.perform(post(BASE_PATH + "/steps/package-response").header("Authorization", authorization) .contentType("application/json").content(step(verificationId))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED")) .andExpect(jsonPath("$.data.confirmationMessage").value(org.hamcrest.Matchers.containsString( "\"status\":\"CONFIRMED\""))); mvc.perform(post(BASE_PATH + "/steps/return-response").header("Authorization", authorization) .contentType("application/json").content(step(verificationId))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED")); mvc.perform(get(BASE_PATH + "/current").header("Authorization", authorization)) .andExpect(status().isOk()).andExpect(jsonPath("$.data.verificationId").value(verificationId)) .andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED")); mvc.perform(get(BASE_PATH).header("Authorization", authorization).param("verificationId", String.valueOf(verificationId))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RESPONSE_RETURNED")); CurrencyGenerationVerificationEntity stored = verificationMapper.selectById(verificationId); assertThat(stored.getConfirmationMessage()).contains("CONFIRMED", "cbSignature", "confirmTime"); assertThat(stored.getDeleted()).isFalse(); } @Test void matchDigestWithoutCalculatedDigestReturnsReminderWithoutCountingError() throws Exception { String received = mvc.perform(post(BASE_PATH + "/steps/receive") .header("Authorization", authorization) .contentType("application/json") .content("{\"requestId\":" + requestId + "}")) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); long verificationId = number(received, "$.data.verificationId"); mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization) .contentType("application/json").content(step(verificationId))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value("还未进行摘要运算,请先运算摘要一")); assertThat(scoreMapper.selectCount(new LambdaQueryWrapper() .eq(StuModuleScoreDetailsEntity::getUserId, USER_ID))).isZero(); } @Test void accumulatesDigestMismatchErrors() throws Exception { String received = mvc.perform(post(BASE_PATH + "/steps/receive") .header("Authorization", authorization) .contentType("application/json") .content("{\"requestId\":" + requestId + "}")) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); long verificationId = number(received, "$.data.verificationId"); mvc.perform(post(BASE_PATH + "/steps/verify-signature").header("Authorization", authorization) .contentType("application/json").content(step(verificationId, "publicKey", bankPublicKey))) .andExpect(status().isOk()); mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization) .contentType("application/json").content(digestStep(verificationId, "错误的请求原文"))) .andExpect(status().isOk()); mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization) .contentType("application/json").content(step(verificationId))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString( "摘要一和摘要二不一致"))) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误"))); mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization) .contentType("application/json").content(step(verificationId))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误"))); StuModuleScoreDetailsEntity score = scoreMapper.selectOne( new LambdaQueryWrapper() .eq(StuModuleScoreDetailsEntity::getUserId, USER_ID) .eq(StuModuleScoreDetailsEntity::getSerialNumber, 3)); assertThat(score.getCompletionStatus()).isEqualTo("2"); CurrencyGenerationVerificationEntity stored = verificationMapper.selectById(verificationId); assertThat(stored.getStatus()).isEqualTo("DIGEST_CALCULATED"); assertThat(stored.getDigestMatches()).isNull(); } @Test void accumulatesWrongStepThreePublicAndPrivateKeys() throws Exception { String received = mvc.perform(post(BASE_PATH + "/steps/receive") .header("Authorization", authorization) .contentType("application/json") .content("{\"requestId\":" + requestId + "}")) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); long verificationId = number(received, "$.data.verificationId"); mvc.perform(post(BASE_PATH + "/steps/verify-signature").header("Authorization", authorization) .contentType("application/json") .content(step(verificationId, "publicKey", "BANK_SECOND_SM2_KEY"))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误"))); mvc.perform(post(BASE_PATH + "/steps/verify-signature").header("Authorization", authorization) .contentType("application/json") .content(step(verificationId, "publicKey", bankPublicKey))) .andExpect(status().isOk()); mvc.perform(post(BASE_PATH + "/steps/calculate-digest").header("Authorization", authorization) .contentType("application/json").content(digestStep(verificationId, "ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100"))) .andExpect(status().isOk()); mvc.perform(post(BASE_PATH + "/steps/match-digest").header("Authorization", authorization) .contentType("application/json").content(step(verificationId))) .andExpect(status().isOk()); mvc.perform(post(BASE_PATH + "/steps/verify-quota").header("Authorization", authorization) .contentType("application/json").content(step(verificationId))) .andExpect(status().isOk()); mvc.perform(post(BASE_PATH + "/steps/confirm").header("Authorization", authorization) .contentType("application/json") .content(step(verificationId, "privateKey", InstitutionKeyService.CENTRAL_FIRST_KEY))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误"))); StuModuleScoreDetailsEntity score = scoreMapper.selectOne( new LambdaQueryWrapper() .eq(StuModuleScoreDetailsEntity::getUserId, USER_ID) .eq(StuModuleScoreDetailsEntity::getSerialNumber, 3)); assertThat(score.getCompletionStatus()).isEqualTo("2"); } @Test void globalQuotaCountsOtherSubjectsAndIgnoresDeletedRequests() { CurrencyGenerationRequestEntity active = quotaRequest("other-user", new BigDecimal("42000.00"), false); requestMapper.insert(active); CurrencyGenerationRequestEntity deleted = quotaRequest("deleted-user", new BigDecimal("7000.00"), true); requestMapper.insert(deleted); assertThat(requestMapper.sumActiveAmountExcluding(requestId)).isEqualByComparingTo("42000.00"); assertThat(requestMapper.sumActiveAmountExcluding(active.getId())).isEqualByComparingTo("1589.85"); } private CurrencyGenerationRequestEntity quotaRequest(String userId, BigDecimal amount, boolean deleted) { LocalDateTime now = LocalDateTime.now(); CurrencyGenerationRequestEntity request = new CurrencyGenerationRequestEntity(); request.setIdentifierApplicationId(requestMapper.selectById(requestId).getIdentifierApplicationId()); request.setInstitutionIdentifier("ORG_GLOBAL_QUOTA"); request.setFullInstitutionIdentifier("ORG_GLOBAL_QUOTA"); request.setAmount(amount); request.setDeliveryNodeCode("SYS_GLOBAL"); request.setRequestTimestamp("20260806210000"); request.setRequestOriginalText("quota"); request.setRequestDigest("A"); request.setBankSignature("B"); request.setSigningKeyId(InstitutionKeyService.BANK_SECOND_KEY); request.setRequestMessage("{}"); request.setStatus("RECEIVED"); request.setUserId(userId); request.setSchoolId(9999L); request.setClassId(9999L); request.setCreatedAt(now); request.setUpdatedAt(now); request.setCreatedBy(userId); request.setUpdatedBy(userId); request.setDeleted(deleted); return request; } private long insertReceivedRequest(InstitutionKeySubject subject) { LocalDateTime now = LocalDateTime.now(); InstitutionIdentifierApplicationEntity application = new InstitutionIdentifierApplicationEntity(); application.setBankCode("BKCHCNBJ30003"); application.setUserId(USER_ID); application.setSchoolId(SCHOOL_ID); application.setClassId(CLASS_ID); application.setRequestTimestamp("20260806190000"); application.setOriginalText("BKCHCNBJ30003|20260806190000"); application.setStatus("FEEDBACKED"); application.setTrainingRound(1); application.setScoringCriteria(0); application.setCreatedAt(now); application.setUpdatedAt(now); application.setCreatedBy("step-three-tester"); application.setUpdatedBy("step-three-tester"); application.setDeleted(false); applicationMapper.insert(application); String original = "ORG_3A4B5C6D7E8F|1589.85|SYS_DC_002|20260806190100"; String digest = cryptography.sm3(original); CurrencyGenerationRequestEntity request = new CurrencyGenerationRequestEntity(); request.setIdentifierApplicationId(application.getId()); request.setInstitutionIdentifier("ORG_3A4B5C6D7E8F"); request.setFullInstitutionIdentifier("ORG_3A4B5C6D7E8F"); request.setAmount(new BigDecimal("1589.85")); request.setDeliveryNodeCode("SYS_DC_002"); request.setRequestTimestamp("20260806190100"); request.setRequestOriginalText(original); request.setRequestDigest(digest); request.setBankSignature(cryptography.sign(keyService.commercialBankSecondPrivateKeyOrNull(subject), digest)); request.setSigningKeyId(InstitutionKeyService.BANK_SECOND_KEY); request.setRequestMessage("{\"digest\":\"" + digest + "\"}"); request.setStatus("RECEIVED"); request.setUserId(USER_ID); request.setSchoolId(SCHOOL_ID); request.setClassId(CLASS_ID); request.setCreatedAt(now); request.setUpdatedAt(now); request.setCreatedBy("step-three-tester"); request.setUpdatedBy("step-three-tester"); request.setDeleted(false); requestMapper.insert(request); return request.getId(); } private static String step(long id) { return "{\"verificationId\":" + id + "}"; } private static String digestStep(long id, String originalText) { return "{\"verificationId\":" + id + ",\"requestOriginalText\":\"" + originalText + "\"}"; } private static String step(long id, String key, String value) { return "{\"verificationId\":" + id + ",\"" + key + "\":\"" + value + "\"}"; } private static long number(String json, String path) { return com.jayway.jsonpath.JsonPath.read(json, path).longValue(); } }