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/CurrencyGenerationVerificat...

335 lines
22 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.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<StuModuleScoreDetailsEntity>()
.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<StuModuleScoreDetailsEntity>()
.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<StuModuleScoreDetailsEntity>()
.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.<Number>read(json, path).longValue();
}
}